Compare commits

..

1 Commits

Author SHA1 Message Date
Naoki Takezoe
9cc3a0d36e Optimize Get-Contents API 2021-11-05 20:21:39 +09:00
116 changed files with 997 additions and 2780 deletions

View File

@@ -10,9 +10,9 @@ jobs:
matrix:
java: [8, 11, 17]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Cache
uses: actions/cache@v3
uses: actions/cache@v2.1.6
env:
cache-name: cache-sbt-libs
with:
@@ -22,18 +22,18 @@ jobs:
~/.cache/coursier/v1
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v2
with:
java-version: ${{ matrix.java }}
distribution: adopt
- name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck test
- name: Scala 3
run: sbt '++ 3.1.2!' update # TODO
run: sbt '++ 3.0.1!' update # TODO
- name: Build executable
run: sbt executable
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: gitbucket-java${{ matrix.java }}-${{ github.sha }}
path: ./target/executable/gitbucket.*

View File

@@ -4,6 +4,4 @@ updates.includeScala = true
updates.pin = [
{ groupId = "org.eclipse.jetty", version = "9." }
{ groupId = "org.eclipse.jgit", version = "5." }
{ groupId = "com.zaxxer", version = "4." }
]

View File

@@ -1,67 +1,32 @@
# Changelog
All changes to the project will be documented in this file.
## 4.38.2 - 20 Sep 2022
- Resurrect assignee icons on the issue list
## 4.38.1 - 10 Sep 2022
- Fix comment diff in Chrome 105
- Fix Markdown table CSS
- Fix HTML rendering of multiple asignees
## 4.38.0 - 3 Sep 2022
- Support multiple assignees for Issues and Pull requests
- Custom fields for issues and pull requests
- Reset password by users
- Allow to configure Jetty idle timeout in standalone mode
- Horizontal scroll for too wide tables in Markdown
- Hide header content on signin and register page
- Fix the default charset of the online editor in the repository viewer
- Fix the milestone count
- Some improvements and bugfixes for WebAPI and WebHook
## 4.37.2 - 16 Jan 2022
- Fixed a security issue reported by [Positive Technologies](https://www.ptsecurity.com/ww-en/). Great thanks for their detailed report and close support!
## 4.37.1 - 14 Dec 2021
- Update gist-plugin and notification-plugin
- Fix SSHCommand extension point for apache-sshd 2.x
## 4.37.0 - 11 Dec 2021
- Enhance Git Reference APIs
- Add milestone data to issue list API
- Support "all" in issue list API
- Support EDDSA in signed commit verification
- Support custom SSH url
- Relax max passward length limitation
- Relax max webhook url length limitation
## 4.36.2 - 16 Aug 2021
### 4.36.2 - 16 Aug 2021
- Escape user name in avatar image tag
## 4.36.1 - 22 Jul 2021
### 4.36.1 - 22 Jul 2021
- Bump gitbucket-gist-plugin to 4.21.0
## 4.36.0 - 17 Jul 2021
### 4.36.0 - 17 Jul 2021
- Tag selector in the repository viewer
- Link issues/pull requests of other repositories
- Files and lines can be linked in the diff view
- Option to disable XSS protection
## 4.35.3 - 14 Jan 2021
### 4.35.3 - 14 Jan 2021
- Fix a bug that Wiki page cannot be deleted
- Fix a deployment issue on Tomcat
## 4.35.2 - 30 Dec 2020
### 4.35.2 - 30 Dec 2020
- Upgrade gitbucket-notifications-plugin to 1.10.0
- Upgrade oauth2-oidc-sdk to 8.29.1 to solve dependency issue
## 4.35.1 - 29 Dec 2020
### 4.35.1 - 29 Dec 2020
- Fix database migration issue which happens if webhook is configured
- Call push webhook when pull request is merged
- Show commit status at commits tab of pull request
## 4.35.0 - 25 Dec 2020
### 4.35.0 - 25 Dec 2020
- Editor and source viewer color theme
- Auto completion for issues and pull requests
- Upload image from clipboard

View File

@@ -1,4 +1,4 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![gitbucket Scala version support](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket/latest-by-scala-version.svg)](https://index.scala-lang.org/gitbucket/gitbucket/gitbucket) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.svg)](https://gitter.im/gitbucket/gitbucket) [![build](https://github.com/gitbucket/gitbucket/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket/actions?query=workflow%3Abuild+branch%3Amaster) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.gitbucket/gitbucket_2.13) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gitbucket/gitbucket/blob/master/LICENSE)
=========
GitBucket is a Git web platform powered by Scala offering:
@@ -10,6 +10,8 @@ GitBucket is a Git web platform powered by Scala offering:
![GitBucket](https://gitbucket.github.io/img/screenshots/screenshot-repository_viewer.png)
You can try an [online demo](https://gitbucket.herokuapp.com/) *(ID: root / Pass: root)* of GitBucket, and also get the latest information at [GitBucket News](https://gitbucket.github.io/gitbucket-news/).
Features
--------
The current version of GitBucket provides many features such as:
@@ -29,7 +31,7 @@ GitBucket requires **Java8**. You have to install it, if it is not already insta
1. Download the latest **gitbucket.war** from [the releases page](https://github.com/gitbucket/gitbucket/releases) and run it by `java -jar gitbucket.war`.
2. Go to `http://[hostname]:8080/` and log in with ID: **root** / Pass: **root**.
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc). Note that GitBucket doesn't support Jakarta EE yet.
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc)
For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
@@ -59,25 +61,18 @@ Support
- If you can't find same question and report, send it to our [Gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
What's New in 4.38.x
What's New in 4.36.x
-------------
## 4.38.2 - 20 Sep 2022
- Resurrect assignee icons on the issue list
### 4.36.2 - 16 Aug 2021
- Escape user name in avatar image tag
## 4.38.1 - 10 Sep 2022
- Fix comment diff in Chrome 105
- Fix Markdown table CSS
- Fix HTML rendering of multiple asignees
### 4.36.1 - 22 Jul 2021
- Bump gitbucket-gist-plugin to 4.21.0
## 4.38.0 - 3 Sep 2022
- Support multiple assignees for Issues and Pull requests
- Custom fields for issues and pull requests
- Reset password by users
- Allow to configure Jetty idle timeout in standalone mode
- Horizontal scroll for too wide tables in Markdown
- Hide header content on signin and register page
- Fix the default charset of the online editor in the repository viewer
- Fix the milestone count
- Some improvements and bugfixes for WebAPI and WebHook
### 4.36.0 - 17 Jul 2021
- Tag selector in the repository viewer
- Link issues/pull requests of other repositories
- Files and lines can be linked in the diff view
- Option to disable XSS protection
See the [change log](CHANGELOG.md) for all of the updates.

View File

@@ -3,10 +3,10 @@ import com.jsuereth.sbtpgp.PgpKeys._
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.38.2"
val GitBucketVersion = "4.36.2"
val ScalatraVersion = "2.8.2"
val JettyVersion = "9.4.49.v20220914"
val JgitVersion = "5.13.1.202206130422-r"
val JettyVersion = "9.4.44.v20210927"
val JgitVersion = "5.13.0.202109080827-r"
lazy val root = (project in file("."))
.enablePlugins(SbtTwirl, ScalatraPlugin)
@@ -15,7 +15,7 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.13.9"
scalaVersion := "2.13.7"
scalafmtOnCompile := true
@@ -24,6 +24,7 @@ coverageExcludedPackages := ".*\\.html\\..*"
// dependency settings
resolvers ++= Seq(
Classpaths.typesafeReleases,
Resolver.jcenterRepo,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
)
@@ -33,42 +34,42 @@ libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-json" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.5" cross CrossVersion.for3Use2_13,
"org.json4s" %% "json4s-jackson" % "4.0.3" cross CrossVersion.for3Use2_13,
"commons-io" % "commons-io" % "2.11.0",
"io.github.gitbucket" % "solidbase" % "1.0.5",
"io.github.gitbucket" % "markedj" % "1.0.17",
"io.github.gitbucket" % "solidbase" % "1.0.3",
"io.github.gitbucket" % "markedj" % "1.0.16",
"org.apache.commons" % "commons-compress" % "1.21",
"org.apache.commons" % "commons-email" % "1.5",
"commons-net" % "commons-net" % "3.8.0",
"org.apache.httpcomponents" % "httpclient" % "4.5.13",
"org.apache.sshd" % "apache-sshd" % "2.9.1" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.4.1",
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
"org.apache.tika" % "tika-core" % "2.1.0",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12" cross CrossVersion.for3Use2_13,
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.199",
"org.mariadb.jdbc" % "mariadb-java-client" % "3.0.7",
"org.postgresql" % "postgresql" % "42.5.0",
"ch.qos.logback" % "logback-classic" % "1.2.11",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.4",
"org.postgresql" % "postgresql" % "42.3.1",
"ch.qos.logback" % "logback-classic" % "1.2.6",
"com.zaxxer" % "HikariCP" % "4.0.3" exclude ("org.slf4j", "slf4j-api"),
"com.typesafe" % "config" % "1.4.2",
"com.typesafe" % "config" % "1.4.1",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
"io.github.java-diff-utils" % "java-diff-utils" % "4.12",
"io.github.java-diff-utils" % "java-diff-utils" % "4.11",
"org.cache2k" % "cache2k-all" % "1.6.0.Final",
"net.coobird" % "thumbnailator" % "0.4.17",
"net.coobird" % "thumbnailator" % "0.4.14",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.43.1",
"com.nimbusds" % "oauth2-oidc-sdk" % "9.19",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.13.2" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test" cross CrossVersion.for3Use2_13,
"org.mockito" % "mockito-core" % "4.8.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.40.10" % "test",
"org.testcontainers" % "mysql" % "1.17.3" % "test",
"org.testcontainers" % "postgresql" % "1.17.3" % "test",
"org.mockito" % "mockito-core" % "4.0.0" % "test",
"com.dimafeng" %% "testcontainers-scala" % "0.39.11" % "test",
"org.testcontainers" % "mysql" % "1.16.2" % "test",
"org.testcontainers" % "postgresql" % "1.16.2" % "test",
"net.i2p.crypto" % "eddsa" % "0.3.0",
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
"org.ec4j.core" % "ec4j-core" % "0.3.0",
"org.kohsuke" % "github-api" % "1.308" % "test"
"org.kohsuke" % "github-api" % "1.135" % "test"
)
libraryDependencies ~= {

View File

@@ -1 +1 @@
sbt.version=1.7.1
sbt.version=1.5.5

View File

@@ -1,11 +1,11 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.4")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.2")
addDependencyTreePlugin

View File

@@ -61,7 +61,6 @@ public class JettyLauncher {
String redirectHttps = getEnvironmentVariable("gitbucket.redirectHttps");
String contextPath = getEnvironmentVariable("gitbucket.prefix");
String tmpDirPath = getEnvironmentVariable("gitbucket.tempDir");
String jettyIdleTimeout = getEnvironmentVariable("gitbucket.jettyIdleTimeout");
boolean saveSessions = false;
for(String arg: args) {
@@ -108,9 +107,6 @@ public class JettyLauncher {
case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]);
break;
case "--jetty_idle_timeout":
jettyIdleTimeout = dim[1];
break;
}
}
}
@@ -134,11 +130,6 @@ public class JettyLauncher {
if (connectorsSet.contains(Connectors.HTTPS)) {
httpConfig.setSecurePort(fallback(securePort, Defaults.HTTPS_PORT, Integer::parseInt));
}
if (jettyIdleTimeout != null && jettyIdleTimeout.trim().length() != 0) {
httpConfig.setIdleTimeout(Long.parseLong(jettyIdleTimeout.trim()));
} else {
httpConfig.setIdleTimeout(300000L); // default is 5min
}
if (connectorsSet.contains(Connectors.HTTP)) {
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));

View File

@@ -1,4 +1,4 @@
notifications:1.11.0
gist:4.22.0
notifications:1.10.0
gist:4.21.0
emoji:4.6.0
pages:1.10.0

View File

@@ -0,0 +1,18 @@
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
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);

View File

@@ -343,28 +343,4 @@
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<!--================================================================================================-->
<!-- ISSUE_OUTLINE_VIEW -->
<!--================================================================================================-->
<sql>
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
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)
</sql>
</changeSet>

View File

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

View File

@@ -35,28 +35,4 @@
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="EVENT" type="varchar(30)" nullable="false"/>
</createTable>
<sql>
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);
</sql>
</changeSet>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/>
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK_EVENT"/>
<modifyDataType columnName="URL" newDataType="varchar(400)" tableName="WEB_HOOK"/>
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/>
</changeSet>

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- CUSTOM_FIELD -->
<!--================================================================================================-->
<createTable tableName="CUSTOM_FIELD">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="FIELD_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
<column name="FIELD_NAME" type="varchar(100)" nullable="false"/>
<column name="FIELD_TYPE" type="varchar(100)" nullable="false"/>
<column name="ENABLE_FOR_ISSUES" type="boolean" nullable="false"/>
<column name="ENABLE_FOR_PULL_REQUESTS" type="boolean" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_CUSTOM_FIELD_PK" tableName="CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
<addForeignKeyConstraint constraintName="IDX_CUSTOM_FIELD_FK0" baseTableName="CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<!--================================================================================================-->
<!-- ISSUE_CUSTOM_FIELD -->
<!--================================================================================================-->
<createTable tableName="ISSUE_CUSTOM_FIELD">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/>
<column name="FIELD_ID" type="int" nullable="false"/>
<column name="VALUE" type="varchar(200)" nullable="true"/>
</createTable>
<addPrimaryKey constraintName="IDX_ISSUE_CUSTOM_FIELD_PK" tableName="ISSUE_CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, FIELD_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK0" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK1" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID" referencedTableName="CUSTOM_FIELD" referencedColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
<!--================================================================================================-->
<!-- ISSUE_ASSIGNEE -->
<!--================================================================================================-->
<createTable tableName="ISSUE_ASSIGNEE">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/>
<column name="ASSIGNEE_USER_NAME" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ISSUE_ASSIGNEE_PK" tableName="ISSUE_ASSIGNEE" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK0" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<!--
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK1" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="ASSIGNEE_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
-->
<sql>
INSERT INTO ISSUE_ASSIGNEE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME)
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNED_USER_NAME FROM ISSUE WHERE ASSIGNED_USER_NAME IS NOT NULL
</sql>
<dropColumn tableName="ISSUE" columnName="ASSIGNED_USER_NAME"/>
</changeSet>

View File

@@ -0,0 +1,2 @@
-- DELETE COLLABORATORS IN GROUP REPOSITORIES
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)

View File

@@ -30,9 +30,4 @@
<dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/>
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
<!-- DELETE COLLABORATORS IN GROUP REPOSITORIES -->
<sql>
DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE)
</sql>
</changeSet>

View File

@@ -9,7 +9,7 @@ import gitbucket.core.model.Activity
import gitbucket.core.util.Directory.ActivityLog
import gitbucket.core.util.JDBCUtil
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration}
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration}
import io.github.gitbucket.solidbase.model.{Module, Version}
import org.json4s.{Formats, NoTypeHints}
import org.json4s.jackson.Serialization
@@ -20,7 +20,11 @@ import scala.util.Using
object GitBucketCoreModule
extends Module(
"gitbucket-core",
new Version("4.0.0", new LiquibaseMigration("update/gitbucket-core_4.0.xml")),
new Version(
"4.0.0",
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new SqlMigration("update/gitbucket-core_4.0.sql")
),
new Version("4.1.0"),
new Version("4.2.0", new LiquibaseMigration("update/gitbucket-core_4.2.xml")),
new Version("4.2.1"),
@@ -28,7 +32,11 @@ object GitBucketCoreModule
new Version("4.4.0"),
new Version("4.5.0"),
new Version("4.6.0", new LiquibaseMigration("update/gitbucket-core_4.6.xml")),
new Version("4.7.0", new LiquibaseMigration("update/gitbucket-core_4.7.xml")),
new Version(
"4.7.0",
new LiquibaseMigration("update/gitbucket-core_4.7.xml"),
new SqlMigration("update/gitbucket-core_4.7.sql")
),
new Version("4.7.1"),
new Version("4.8"),
new Version("4.9.0", new LiquibaseMigration("update/gitbucket-core_4.9.xml")),
@@ -37,7 +45,11 @@ object GitBucketCoreModule
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 Version(
"4.14.0",
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new SqlMigration("update/gitbucket-core_4.14.sql")
),
new Version("4.14.1"),
new Version("4.15.0"),
new Version("4.16.0"),
@@ -107,11 +119,5 @@ object GitBucketCoreModule
new Version("4.35.3"),
new Version("4.36.0", new LiquibaseMigration("update/gitbucket-core_4.36.xml")),
new Version("4.36.1"),
new Version("4.36.2"),
new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")),
new Version("4.37.1"),
new Version("4.37.2"),
new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")),
new Version("4.38.1"),
new Version("4.38.2")
new Version("4.36.2")
)

View File

@@ -97,7 +97,7 @@ object ApiCommits {
ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id,
html_url = ApiPath(s"/${repositoryName.fullName}/commit/${commitInfo.id}"),
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""), // TODO no API for commit comment
commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),

View File

@@ -12,7 +12,7 @@ case class ApiIssue(
number: Int,
title: String,
user: ApiUser,
assignees: List[ApiUser],
assignee: Option[ApiUser],
labels: List[ApiLabel],
state: String,
created_at: Date,
@@ -21,7 +21,7 @@ case class ApiIssue(
milestone: Option[ApiMilestone]
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
val id = 0 // dummy id
val assignee = assignees.headOption
val assignees = List(assignee).flatten
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
val pull_request = if (isPullRequest) {
@@ -43,7 +43,7 @@ object ApiIssue {
issue: Issue,
repositoryName: RepositoryName,
user: ApiUser,
assignees: List[ApiUser],
assignee: Option[ApiUser],
labels: List[ApiLabel],
milestone: Option[ApiMilestone]
): ApiIssue =
@@ -51,7 +51,7 @@ object ApiIssue {
number = issue.issueId,
title = issue.title,
user = user,
assignees = assignees,
assignee = assignee,
labels = labels,
milestone = milestone,
state = if (issue.closed) { "closed" } else { "open" },

View File

@@ -21,11 +21,10 @@ case class ApiPullRequest(
body: String,
user: ApiUser,
labels: List[ApiLabel],
assignees: List[ApiUser],
assignee: Option[ApiUser],
draft: Option[Boolean]
) {
val id = 0 // dummy id
val assignee = assignees.headOption
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -46,7 +45,7 @@ object ApiPullRequest {
baseRepo: ApiRepository,
user: ApiUser,
labels: List[ApiLabel],
assignees: List[ApiUser],
assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest =
ApiPullRequest(
@@ -64,7 +63,7 @@ object ApiPullRequest {
body = issue.content.getOrElse(""),
user = user,
labels = labels,
assignees = assignees,
assignee = assignee,
draft = Some(pullRequest.isDraft)
)

View File

@@ -1,55 +1,5 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil.TagInfo
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.lib.Ref
case class ApiObject(sha: String)
case class ApiRefCommit(
sha: String,
`type`: String,
url: ApiPath
)
case class ApiRef(
ref: String,
node_id: String = "",
url: ApiPath,
`object`: ApiRefCommit,
)
object ApiRef {
def fromRef(
repositoryName: RepositoryName,
ref: Ref
): ApiRef =
ApiRef(
ref = ref.getName,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/${ref.getName}"),
`object` = ApiRefCommit(
sha = ref.getObjectId.getName,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${ref.getObjectId.getName}"),
`type` = "commit"
)
)
def fromTag(
repositoryName: RepositoryName,
tagInfo: TagInfo
): ApiRef =
ApiRef(
ref = s"refs/tags/${tagInfo.name}",
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/refs/tags/${tagInfo.name}"),
//the GH api distinguishes between "releases" and plain git tags
//for "releases", the api returns a reference to the release object (with type `tag`)
//this would be something like s"/api/v3/repos/${repositoryName.fullName}/git/tags/<hash-of-tag>"
//with a hash for the tag, which I do not fully understand
//since this is not yet implemented in GB, we always return a link to the plain `commit` object,
//which GH does for tags that are not annotated
`object` = ApiRefCommit(
sha = tagInfo.objectId,
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${tagInfo.objectId}"),
`type` = "commit"
)
)
}
case class ApiRef(ref: String, `object`: ApiObject)

View File

@@ -21,7 +21,7 @@ case class ApiRepository(
val url = ApiPath(s"/api/v3/repos/${full_name}")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(""))
val ssh_url = Some(SshPath(s":${full_name}.git"))
}
object ApiRepository {

View File

@@ -0,0 +1,29 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
case class ApiTagCommit(
sha: String,
url: ApiPath
)
case class ApiTag(
name: String,
commit: ApiTagCommit,
zipball_url: ApiPath,
tarball_url: ApiPath
)
object ApiTag {
def apply(
tagName: String,
repositoryName: RepositoryName,
commitId: String
): ApiTag =
ApiTag(
name = tagName,
commit = ApiTagCommit(sha = commitId, url = ApiPath(s"/${repositoryName.fullName}/commits/${commitId}")),
zipball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.zip"),
tarball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.tar.gz")
)
}

View File

@@ -128,15 +128,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"highlighterTheme" -> trim(label("Theme", text(required)))
)(SyntaxHighlighterThemeForm.apply)
val resetPasswordEmailForm = mapping(
"mailAddress" -> trim(label("Email", text(required)))
)(ResetPasswordEmailForm.apply)
val resetPasswordForm = mapping(
"token" -> trim(label("Token", text(required))),
"password" -> trim(label("Password", text(required, maxlength(40))))
)(ResetPasswordForm.apply)
case class NewGroupForm(
groupName: String,
description: Option[String],
@@ -152,13 +143,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
members: String,
clearImage: Boolean
)
case class ResetPasswordEmailForm(
mailAddress: String
)
case class ResetPasswordForm(
token: String,
password: String
)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
@@ -618,7 +602,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
})
get("/register") {
if (context.settings.basicBehavior.allowAccountRegistration) {
if (context.settings.allowAccountRegistration) {
if (context.loginAccount.isDefined) {
redirect("/")
} else {
@@ -628,7 +612,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
post("/register", newForm) { form =>
if (context.settings.basicBehavior.allowAccountRegistration) {
if (context.settings.allowAccountRegistration) {
createAccount(
form.userName,
pbkdf2_sha256(form.password),
@@ -644,63 +628,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} else NotFound()
}
get("/reset") {
if (context.settings.basicBehavior.allowResetPassword) {
html.reset()
} else NotFound()
}
post("/reset", resetPasswordEmailForm) { form =>
if (context.settings.basicBehavior.allowResetPassword) {
getAccountByMailAddress(form.mailAddress).foreach { account =>
val token = generateResetPasswordToken(form.mailAddress)
val mailer = new Mailer(context.settings)
mailer.send(
form.mailAddress,
"Reset password",
s"""Hello, ${account.fullName}!
|
|You requested to reset the password for your GitBucket account.
|If you are not sure about the request, you can ignore this email.
|Otherwise, click the following link to set the new password:
|${context.baseUrl}/reset/form/${token}
|""".stripMargin
)
}
redirect("/reset/sent")
} else NotFound()
}
get("/reset/sent") {
if (context.settings.basicBehavior.allowResetPassword) {
html.resetsent()
} else NotFound()
}
get("/reset/form/:token") {
if (context.settings.basicBehavior.allowResetPassword) {
val token = params("token")
decodeResetPasswordToken(token)
.map { _ =>
html.resetform(token)
}
.getOrElse(NotFound())
} else NotFound()
}
post("/reset/form", resetPasswordForm) { form =>
if (context.settings.basicBehavior.allowResetPassword) {
decodeResetPasswordToken(form.token)
.flatMap { mailAddress =>
getAccountByMailAddress(mailAddress).map { account =>
updateAccount(account.copy(password = pbkdf2_sha256(form.password)))
html.resetcomplete()
}
}
.getOrElse(NotFound())
} else NotFound()
}
get("/groups/new")(usersOnly {
context.withLoginAccount { loginAccount =>
html.creategroup(List(GroupMember("", loginAccount.userName, true)))
@@ -786,7 +713,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/
get("/new")(usersOnly {
context.withLoginAccount { loginAccount =>
html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.basicBehavior.isCreateRepoOptionPublic)
html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.isCreateRepoOptionPublic)
}
})
@@ -796,7 +723,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/new", newRepositoryForm)(usersOnly { form =>
context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) {
if (context.settings.repositoryOperation.create || loginAccount.isAdmin) {
LockUtil.lock(s"${form.owner}/${form.name}") {
if (getRepository(form.owner, form.name).isDefined) {
// redirect to the repository if repository already exists
@@ -826,7 +753,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
context.withLoginAccount {
loginAccount =>
if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
@@ -853,7 +780,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
context.withLoginAccount {
loginAccount =>
if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) {
if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) {
val loginUserName = loginAccount.userName
val accountName = form.accountName

View File

@@ -40,7 +40,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
limit = context.settings.limitVisibleRepositories
)
html.repos(getGroupNames(loginAccount.userName), repos, repos)
}
@@ -129,7 +129,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
limit = context.settings.limitVisibleRepositories
)
)
}
@@ -171,7 +171,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
limit = context.settings.limitVisibleRepositories
)
)
}

View File

@@ -69,7 +69,7 @@ trait IndexControllerBase extends ControllerBase {
Some(account),
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
limit = context.settings.limitVisibleRepositories
),
showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken(
account.userName
@@ -289,11 +289,11 @@ trait IndexControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.basicBehavior.limitVisibleRepositories
limit = context.settings.limitVisibleRepositories
)
val repositories = {
context.settings.basicBehavior.limitVisibleRepositories match {
context.settings.limitVisibleRepositories match {
case true =>
getVisibleRepositories(
context.loginAccount,

View File

@@ -1,7 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.issues.html
import gitbucket.core.model.{Account, CustomFieldBehavior}
import gitbucket.core.model.Account
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
@@ -21,7 +21,6 @@ class IssuesController
with ActivityService
with HandleCommentService
with IssueCreationService
with CustomFieldsService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
@@ -42,7 +41,6 @@ trait IssuesControllerBase extends ControllerBase {
with ActivityService
with HandleCommentService
with IssueCreationService
with CustomFieldsService
with ReadableUsersAuthenticator
with ReferrerAuthenticator
with WritableUsersAuthenticator
@@ -53,7 +51,7 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(
title: String,
content: Option[String],
assigneeUserNames: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
@@ -64,7 +62,7 @@ trait IssuesControllerBase extends ControllerBase {
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())),
"assigneeUserNames" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
@@ -107,12 +105,10 @@ trait IssuesControllerBase extends ControllerBase {
issue,
getComments(repository.owner, repository.name, issueId.toInt),
getIssueLabels(repository.owner, repository.name, issueId.toInt),
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
isIssueEditable(repository),
isIssueManageable(repository),
isIssueCommentManageable(repository),
@@ -130,7 +126,6 @@ trait IssuesControllerBase extends ControllerBase {
getPriorities(repository.owner, repository.name),
getDefaultPriority(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFields(repository.owner, repository.name).filter(_.enableForIssues),
isIssueManageable(repository),
getContentTemplate(repository, "ISSUE_TEMPLATE"),
repository
@@ -146,31 +141,12 @@ trait IssuesControllerBase extends ControllerBase {
repository,
form.title,
form.content,
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
form.assignedUserName,
form.milestoneId,
form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")),
loginAccount
)
// Insert custom field values
params.toMap.foreach {
case (key, value) =>
if (key.startsWith("custom-field-")) {
getCustomField(
repository.owner,
repository.name,
key.replaceFirst("^custom-field-", "").toInt
).foreach { field =>
CustomFieldBehavior.validate(field, value, messages) match {
case None =>
insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
case Some(_) => halt(400)
}
}
}
}
redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
} else Unauthorized()
}
@@ -259,7 +235,7 @@ trait IssuesControllerBase extends ControllerBase {
loginAccount =>
getComment(repository.owner, repository.name, params("id")).map { comment =>
if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
updateComment(comment.issueId, comment.commentId, form.content)
redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized()
} getOrElse NotFound()
@@ -357,16 +333,15 @@ trait IssuesControllerBase extends ControllerBase {
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
})
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
val issueId = params("id").toInt
registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok()
})
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
val issueId = params("id").toInt
deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok()
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
updateAssignedUserName(
repository.owner,
repository.name,
params("id").toInt,
assignedUserName("assignedUserName"),
true
)
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
@@ -387,35 +362,6 @@ trait IssuesControllerBase extends ControllerBase {
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/customfield_validation/:fieldId")(writableUsersOnly { repository =>
val fieldId = params("fieldId").toInt
val value = params("value")
getCustomField(repository.owner, repository.name, fieldId)
.flatMap { field =>
CustomFieldBehavior.validate(field, value, messages).map { error =>
Ok(error)
}
}
.getOrElse(Ok())
})
ajaxPost("/:owner/:repository/issues/:id/customfield/:fieldId")(writableUsersOnly { repository =>
val issueId = params("id").toInt
val fieldId = params("fieldId").toInt
val value = params("value")
for {
_ <- getIssue(repository.owner, repository.name, issueId.toString)
field <- getCustomField(repository.owner, repository.name, fieldId)
} {
CustomFieldBehavior.validate(field, value, messages) match {
case None => insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issueId, value)
case Some(_) => halt(400)
}
}
Ok(value)
})
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
val action = params.get("value")
action match {
@@ -457,13 +403,7 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
val value = assignedUserName("value")
executeBatch(repository) {
//updateAssignedUserName(repository.owner, repository.name, _, value, true)
value match {
case Some(assignedUserName) =>
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
case None =>
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
}
updateAssignedUserName(repository.owner, repository.name, _, value, true)
}
if (params("uri").nonEmpty) {
redirect(params("uri"))

View File

@@ -44,7 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -59,7 +59,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -76,7 +76,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)

View File

@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
* If anonymous access is allowed, pass all requests.
* But if it's not allowed, demands authentication except some paths.
*/
get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") &&
!context.currentPath.startsWith("/plugin-assets") &&

View File

@@ -45,7 +45,7 @@ trait PrioritiesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
html.list(
getPriorities(repository.owner, repository.name),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -60,7 +60,7 @@ trait PrioritiesControllerBase extends ControllerBase {
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()),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -84,7 +84,7 @@ trait PrioritiesControllerBase extends ControllerBase {
)
html.priority(
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)

View File

@@ -25,7 +25,6 @@ class PullRequestsController
with PullRequestService
with MilestonesService
with LabelsService
with CustomFieldsService
with CommitsService
with ActivityService
with WebHookPullRequestService
@@ -45,7 +44,6 @@ trait PullRequestsControllerBase extends ControllerBase {
with IssuesService
with MilestonesService
with LabelsService
with CustomFieldsService
with CommitsService
with ActivityService
with PullRequestService
@@ -92,7 +90,7 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdFrom: String,
commitIdTo: String,
isDraft: Boolean,
assignedUserNames: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
@@ -131,12 +129,10 @@ trait PullRequestsControllerBase extends ControllerBase {
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
getIssueLabels(repository.owner, repository.name, issueId),
getIssueAssignees(repository.owner, repository.name, issueId),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
getLabels(repository.owner, repository.name),
getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests),
isEditable(repository),
isManageable(repository),
hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount),
@@ -509,8 +505,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getMilestones(originRepository.owner, originRepository.name),
getPriorities(originRepository.owner, originRepository.name),
getDefaultPriority(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name),
getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests)
getLabels(originRepository.owner, originRepository.name)
)
}
case (oldId, newId) =>
@@ -572,6 +567,7 @@ trait PullRequestsControllerBase extends ControllerBase {
loginUser = loginAccount.userName,
title = form.title,
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
@@ -591,14 +587,8 @@ trait PullRequestsControllerBase extends ControllerBase {
settings = context.settings
)
// insert labels
if (manageable) {
// insert assignees
form.assignedUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
}
}
// insert labels
form.labelNames.foreach { value =>
val labels = getLabels(repository.owner, repository.name)
value.split(",").foreach { labelName =>

View File

@@ -138,7 +138,7 @@ trait ReleaseControllerBase extends ControllerBase {
get("/:owner/:repository/changelog/*...*")(writableUsersOnly { repository =>
val Seq(previousTag, currentTag) = multiParams("splat")
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.commitId }.getOrElse("")
val previousTagId = repository.tags.collectFirst { case x if x.name == previousTag => x.id }.getOrElse("")
val commitLog = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val commits = JGitUtil.getCommitLog(git, previousTagId, currentTag).reverse

View File

@@ -2,6 +2,7 @@ package gitbucket.core.controller
import java.time.{LocalDateTime, ZoneOffset}
import java.util.Date
import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook}
import gitbucket.core.service._
@@ -20,7 +21,7 @@ import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
import scala.util.Using
import org.scalatra.{Forbidden, Ok}
import org.scalatra.Forbidden
class RepositorySettingsController
extends RepositorySettingsControllerBase
@@ -30,7 +31,6 @@ class RepositorySettingsController
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with CustomFieldsService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator
@@ -43,7 +43,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
with ProtectedBranchService
with CommitStatusService
with DeployKeyService
with CustomFieldsService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator =>
@@ -122,21 +121,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"newOwner" -> trim(label("New owner", text(required, transferUser)))
)(TransferOwnerShipForm.apply)
// for custom field
case class CustomFieldForm(
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
val customFieldForm = mapping(
"fieldName" -> trim(label("Field name", text(required, maxlength(100)))),
"fieldType" -> trim(label("Field type", text(required))),
"enableForIssues" -> trim(label("Enable for issues", boolean(required))),
"enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))),
)(CustomFieldForm.apply)
/**
* Redirect to the Options page.
*/
@@ -406,7 +390,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) {
if (context.settings.repositoryOperation.rename || loginAccount.isAdmin) {
if (repository.name != form.repositoryName) {
// Update database and move git repository
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
@@ -430,7 +414,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
context.withLoginAccount {
loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) {
if (context.settings.repositoryOperation.transfer || loginAccount.isAdmin) {
// Change repository owner
if (repository.owner != form.newOwner) {
// Update database and move git repository
@@ -454,7 +438,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/
post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
context.withLoginAccount { loginAccount =>
if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) {
if (context.settings.repositoryOperation.delete || loginAccount.isAdmin) {
// Delete the repository and related files
deleteRepository(repository.repository)
redirect(s"/${repository.owner}")
@@ -493,58 +477,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/settings/deploykey")
})
/** Custom fields for issues and pull requests */
get("/:owner/:repository/settings/issues")(ownerOnly { repository =>
val customFields = getCustomFields(repository.owner, repository.name)
html.issues(customFields, repository)
})
/** New custom field form */
get("/:owner/:repository/settings/issues/fields/new")(ownerOnly { repository =>
html.issuesfieldform(None, repository)
})
/** Add custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/new", customFieldForm)(ownerOnly { (form, repository) =>
val fieldId = createCustomField(
repository.owner,
repository.name,
form.fieldName,
form.fieldType,
form.enableForIssues,
form.enableForPullRequests
)
html.issuesfield(getCustomField(repository.owner, repository.name, fieldId).get)
})
/** Edit custom field form */
ajaxGet("/:owner/:repository/settings/issues/fields/:fieldId/edit")(ownerOnly { repository =>
getCustomField(repository.owner, repository.name, params("fieldId").toInt).map { customField =>
html.issuesfieldform(Some(customField), repository)
} getOrElse NotFound()
})
/** Update custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/edit", customFieldForm)(ownerOnly {
(form, repository) =>
updateCustomField(
repository.owner,
repository.name,
params("fieldId").toInt,
form.fieldName,
form.fieldType,
form.enableForIssues,
form.enableForPullRequests
)
html.issuesfield(getCustomField(repository.owner, repository.name, params("fieldId").toInt).get)
})
/** Delete custom field */
ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/delete")(ownerOnly { repository =>
deleteCustomField(repository.owner, repository.name, params("fieldId").toInt)
Ok()
})
/**
* Provides duplication check for web hook url.
*/

View File

@@ -329,12 +329,50 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
context.withLoginAccount {
loginAccount =>
val files = form.uploadFiles
.split("\n")
.map { line =>
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
.toSeq
val newFiles = files.map { file =>
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
}
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, newFiles, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, newFiles, loginAccount)
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}")
}
}
}
def _commit(
branchName: String,
//files: Seq[CommitFile],
newFiles: Seq[CommitFile],
loginAccount: Account
): Either[String, ObjectId] = {
): ObjectId = {
commitFiles(
repository = repository,
branch = branchName,
@@ -361,52 +399,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
}
context.withLoginAccount {
loginAccount =>
val files = form.uploadFiles
.split("\n")
.map { line =>
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
.toSeq
val newFiles = files.map { file =>
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
}
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, newFiles, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, newFiles, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else {
redirect(
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
}
})
get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
@@ -464,7 +456,32 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, loginAccount)
redirect(
s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName)
else s"${form.path}/${urlEncode(form.newFileName)}"}"
)
}
}
def _commit(branchName: String, loginAccount: Account): ObjectId = {
commitFile(
repository = repository,
branch = branchName,
@@ -477,48 +494,37 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit,
loginAccount = loginAccount,
settings = context.settings
).map(_._1)
}
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
)
} else {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
)._1
}
})
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, loginAccount)
redirect(
s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName)
else s"${form.path}/${urlEncode(form.newFileName)}"}"
)
}
}
def _commit(branchName: String, loginAccount: Account): ObjectId = {
commitFile(
repository = repository,
branch = branchName,
@@ -535,48 +541,37 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit,
loginAccount = loginAccount,
settings = context.settings
).map(_._1)
}
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${urlEncode(form.newFileName)}"
)
} else {
redirect(
s"/${repository.owner}/${repository.name}/blob/${encodeRefName(form.branch)}/${encodeRefName(form.path)}/${urlEncode(form.newFileName)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
)._1
}
})
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
def _commit(branchName: String, loginAccount: Account): Either[String, ObjectId] = {
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
val objectId = _commit(newBranchName, loginAccount)
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} else {
_commit(form.branch, loginAccount)
redirect(
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) ""
else "/" + form.path}"
)
}
}
def _commit(branchName: String, loginAccount: Account): ObjectId = {
commitFile(
repository = repository,
branch = branchName,
@@ -589,41 +584,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit,
loginAccount = loginAccount,
settings = context.settings
).map(_._1)
}
context.withLoginAccount {
loginAccount =>
if (form.newBranch) {
val newBranchName = createNewBranchForPullRequest(repository, form.branch, loginAccount)
_commit(newBranchName, loginAccount) match {
case Right(objectId) =>
val issueId =
createIssueAndPullRequest(
repository,
form.branch,
newBranchName,
form.commit,
objectId.name,
form.message,
loginAccount
)
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
} else {
_commit(form.branch, loginAccount) match {
case Right(_) =>
if (form.path.length == 0) {
redirect(s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}")
} else {
redirect(
s"/${repository.owner}/${repository.name}/tree/${encodeRefName(form.branch)}/${encodeRefName(form.path)}"
)
}
case Left(error) => Forbidden(gitbucket.core.html.error(error))
}
}
)._1
}
})
@@ -679,6 +640,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loginUser = loginAccount.userName,
title = requestBranch,
content = commitMessage,
assignedUserName = None,
milestoneId = None,
priorityId = None,
isPullRequest = true

View File

@@ -1,6 +1,7 @@
package gitbucket.core.controller
import java.io.FileInputStream
import gitbucket.core.admin.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService._
@@ -34,38 +35,23 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"basicBehavior" -> mapping(
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowResetPassword" -> trim(label("Reset password", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
"repositoryOperation" -> mapping(
"create" -> trim(label("Allow all users to create repository", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())),
"rename" -> trim(label("Allow all users to rename repository", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
"fork" -> trim(label("Allow all users to fork repository", boolean()))
)(RepositoryOperation.apply),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
)(BasicBehavior.apply),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())),
"repositoryOperation" -> mapping(
"create" -> trim(label("Allow all users to create repository", boolean())),
"delete" -> trim(label("Allow all users to delete repository", boolean())),
"rename" -> trim(label("Allow all users to rename repository", boolean())),
"transfer" -> trim(label("Allow all users to transfer repository", boolean())),
"fork" -> trim(label("Allow all users to fork repository", boolean()))
)(RepositoryOperation.apply),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
"ssh" -> mapping(
"enabled" -> trim(label("SSH access", boolean())),
"bindAddress" -> mapping(
"host" -> trim(label("Bind SSH host", optional(text()))),
"port" -> trim(label("Bind SSH port", optional(number()))),
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(DefaultSshPort), GenericSshUser))
),
"publicAddress" -> mapping(
"host" -> trim(label("Public SSH host", optional(text()))),
"port" -> trim(label("Public SSH port", optional(number()))),
)(
(hostOption, portOption) =>
hostOption.map(h => SshAddress(h, portOption.getOrElse(PublicSshPort), GenericSshUser))
),
"host" -> trim(label("SSH host", optional(text()))),
"port" -> trim(label("SSH port", optional(number())))
)(Ssh.apply),
"useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked(
@@ -130,8 +116,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None,
if (settings.ssh.enabled && settings.ssh.bindAddress.isEmpty) {
Some("ssh.bindAddress.host" -> "SSH bind host is required if SSH access is enabled.")
if (settings.ssh.enabled && settings.ssh.sshHost.isEmpty) {
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
} else None
).flatten
}
@@ -322,13 +308,12 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if (form.ssh.bindAddress != context.settings.sshBindAddress || form.ssh.publicAddress != context.settings.sshPublicAddress) {
if (form.sshAddress != context.settings.sshAddress) {
SshServer.stop()
for {
bindAddress <- form.ssh.bindAddress
publicAddress <- form.ssh.publicAddress.orElse(form.ssh.bindAddress)
sshAddress <- form.sshAddress
baseUrl <- form.baseUrl
} SshServer.start(bindAddress, publicAddress, baseUrl)
} SshServer.start(sshAddress, baseUrl)
}
flash.update("info", "System settings has been updated.")
@@ -337,12 +322,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try {
new Mailer(
context.settings.copy(
smtp = Some(form.smtp),
basicBehavior = context.settings.basicBehavior.copy(notification = true)
)
).send(
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
to = form.testAddress,
subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.",

View File

@@ -1,10 +1,9 @@
package gitbucket.core.controller.api
import gitbucket.core.api.{ApiError, ApiRef, CreateARef, JsonFormat, UpdateARef}
import gitbucket.core.api.{ApiObject, ApiRef, CreateARef, JsonFormat, UpdateARef}
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.ReferrerAuthenticator
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{ReferrerAuthenticator, RepositoryName, WritableUsersAuthenticator}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.RefUpdate.Result
@@ -15,39 +14,48 @@ import scala.jdk.CollectionConverters._
import scala.util.Using
trait ApiGitReferenceControllerBase extends ControllerBase {
self: ReferrerAuthenticator with WritableUsersAuthenticator =>
self: ReferrerAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[ApiGitReferenceControllerBase])
get("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
val result = Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val refs = git
.getRepository()
.getRefDatabase()
.getRefsByPrefix("refs")
.asScala
refs.map(ApiRef.fromRef(RepositoryName(s"${repository.owner}/${repository.name}"), _))
}
JsonFormat(result)
})
/*
* i. Get a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference
*/
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
val revstr = multiParams("splat").head
getRef(revstr, repository)
getRef()
})
// Some versions of GHE support this path
get("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { repository =>
logger.warn("git/refs/ endpoint may not be compatible with GitHub API v3. Consider using git/ref/ endpoint instead")
val revstr = multiParams("splat").head
getRef(revstr, repository)
getRef()
})
private def getRef() = {
val revstr = multiParams("splat").head
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository().findRef(revstr)
if (ref != null) {
val sha = ref.getObjectId().name()
JsonFormat(ApiRef(revstr, ApiObject(sha)))
} else {
val refs = git
.getRepository()
.getRefDatabase()
.getRefsByPrefix("refs/")
.asScala
JsonFormat(refs.map { ref =>
val sha = ref.getObjectId().name()
ApiRef(revstr, ApiObject(sha))
})
}
}
}
/*
* ii. Get all references
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#list-matching-references
@@ -57,17 +65,17 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iii. Create a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
*/
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { repository =>
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { _ =>
extractFromJsonBody[CreateARef].map {
data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository.findRef(data.ref)
if (ref == null) {
val update = git.getRepository.updateRef(data.ref)
update.setNewObjectId(ObjectId.fromString(data.sha))
val result = update.update()
result match {
case Result.NEW => JsonFormat(ApiRef.fromRef(RepositoryName(repository.owner, repository.name), ref))
case Result.NEW => JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
case _ => UnprocessableEntity(result.name())
}
} else {
@@ -81,11 +89,11 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* iv. Update a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
*/
patch("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { repository =>
patch("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
val refName = multiParams("splat").mkString("/")
extractFromJsonBody[UpdateARef].map {
data =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.owner))) { git =>
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository.findRef(refName)
if (ref == null) {
UnprocessableEntity("Ref does not exist.")
@@ -96,7 +104,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
val result = update.update()
result match {
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
JsonFormat(ApiRef.fromRef(RepositoryName(repository), update.getRef))
JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
case _ => UnprocessableEntity(result.name())
}
}
@@ -108,7 +116,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
* v. Delete a reference
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference
*/
delete("/api/v3/repos/:owner/:repository/git/refs/*")(writableUsersOnly { _ =>
delete("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
val refName = multiParams("splat").mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val ref = git.getRepository.findRef(refName)
@@ -125,34 +133,4 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
}
}
})
private def notFound(): ApiError = {
response.setStatus(404)
ApiError("Not Found")
}
protected def getRef(revstr: String, repository: RepositoryInfo): AnyRef = {
logger.debug(s"getRef: path '${revstr}'")
val name = RepositoryName(repository)
val result = JsonFormat(revstr match {
case "tags" => repository.tags.map(ApiRef.fromTag(name, _))
case x if x.startsWith("tags/") =>
val tagName = x.substring("tags/".length)
repository.tags.find(_.name == tagName) match {
case Some(tagInfo) => ApiRef.fromTag(name, tagInfo)
case None => notFound()
}
case other =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository().findRef(other) match {
case null => notFound()
case ref => ApiRef.fromRef(name, ref)
}
}
})
logger.debug(s"json result: $result")
result
}
}

View File

@@ -29,9 +29,9 @@ trait ApiIssueControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
//val baseOwner = getAccountByUserName(repository.owner).get
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, List[Account])] =
val issues: List[(Issue, Account, Option[Account])] =
searchIssueByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -40,12 +40,12 @@ trait ApiIssueControllerBase extends ControllerBase {
)
JsonFormat(issues.map {
case (issue, issueUser, assigneeUsers) =>
case (issue, issueUser, assignedUser) =>
ApiIssue(
issue = issue,
repositoryName = RepositoryName(repository),
user = ApiUser(issueUser),
assignees = assigneeUsers.map(ApiUser(_)),
assignee = assignedUser.map(ApiUser(_)),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
@@ -61,8 +61,7 @@ trait ApiIssueControllerBase extends ControllerBase {
(for {
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
assigneeUsers = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName), Set())
users = getAccountsByUserNames(Set(issue.openedUserName) ++ issue.assignedUserName, Set())
openedUser <- users.get(issue.openedUserName)
} yield {
JsonFormat(
@@ -70,7 +69,7 @@ trait ApiIssueControllerBase extends ControllerBase {
issue,
RepositoryName(repository),
ApiUser(openedUser),
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
)
@@ -93,7 +92,7 @@ trait ApiIssueControllerBase extends ControllerBase {
repository,
data.title,
data.body,
data.assignees,
data.assignees.headOption,
milestone.map(_.milestoneId),
None,
data.labels,
@@ -104,9 +103,7 @@ trait ApiIssueControllerBase extends ControllerBase {
issue,
RepositoryName(repository),
ApiUser(loginAccount),
getIssueAssignees(repository.owner, repository.name, issue.issueId)
.flatMap(x => getAccountByUserName(x.assigneeUserName, false))
.map(ApiUser.apply),
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }

View File

@@ -2,6 +2,7 @@ package gitbucket.core.controller.api
import gitbucket.core.api._
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.MilestonesService
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import org.scalatra.NoContent

View File

@@ -40,7 +40,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] =
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -49,7 +49,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
)
JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
@@ -58,7 +58,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignees = assignees.map(ApiUser.apply),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
@@ -99,6 +99,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
loginUser = context.loginAccount.get.userName,
title = createPullReq.title,
content = createPullReq.body,
assignedUserName = None,
milestoneId = None,
priorityId = None,
isPullRequest = true
@@ -318,8 +319,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignees = getIssueAssignees(repository.owner, repository.name, issueId).flatMap { assignedUser =>
getAccountByUserName(assignedUser.assigneeUserName, false)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
@@ -331,7 +332,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignees = assignees.map(ApiUser.apply),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
}

View File

@@ -9,23 +9,9 @@ import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommi
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.revwalk.filter.{
AndRevFilter,
AuthorRevFilter,
CommitTimeRevFilter,
MaxCountRevFilter,
RevFilter,
SkipRevFilter
}
import org.eclipse.jgit.treewalk.filter.{AndTreeFilter, PathFilterGroup, TreeFilter}
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters._
import scala.util.Using
import math.min
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter._
import java.util.Date
import java.time.ZoneOffset
trait ApiRepositoryCommitControllerBase extends ControllerBase {
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
@@ -36,13 +22,8 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params.get("sha").filter(_.nonEmpty).getOrElse("HEAD")
val page = params.get("page").filter(_.nonEmpty).getOrElse("1").toInt
val per_page = min(params.get("per_page").filter(_.nonEmpty).getOrElse("30").toInt, 100)
val author = params.get("author").filter(_.nonEmpty)
val path = params.get("path").filter(_.nonEmpty)
val since = params.get("since").filter(_.nonEmpty)
val until = params.get("until").filter(_.nonEmpty)
// TODO: The following parameters need to be implemented. [:path, :author, :since, :until]
val sha = params.getOrElse("sha", "refs/heads/master")
Using.resource(Git.open(getRepositoryDir(owner, name))) {
git =>
val repo = git.getRepository
@@ -50,37 +31,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
revWalk =>
val objectId = repo.resolve(sha)
revWalk.markStart(revWalk.parseCommit(objectId))
if (path.nonEmpty) {
revWalk.setTreeFilter(
AndTreeFilter.create(PathFilterGroup.createFromStrings(path.get), TreeFilter.ANY_DIFF)
)
}
val revfilters = new ListBuffer[(RevFilter)]()
if (author.nonEmpty) {
revfilters += AuthorRevFilter.create(author.get)
}
if (since.nonEmpty) {
revfilters += CommitTimeRevFilter.after(
Date.from(LocalDateTime.parse(since.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (until.nonEmpty) {
revfilters += CommitTimeRevFilter.before(
Date.from(LocalDateTime.parse(until.get, ISO_DATE_TIME).toInstant(ZoneOffset.UTC))
)
}
if (page > 1) {
revfilters += SkipRevFilter.create(page * per_page - 2)
}
revfilters += MaxCountRevFilter.create(per_page);
revWalk.setRevFilter(
if (revfilters.size > 1) {
AndRevFilter.create(revfilters.toArray)
} else {
revfilters(0)
}
)
JsonFormat(revWalk.asScala.map {
JsonFormat(revWalk.asScala.take(30).map {
commit =>
val commitInfo = new CommitInfo(commit)
ApiCommits(

View File

@@ -3,7 +3,7 @@ import gitbucket.core.api.{ApiCommit, ApiContents, ApiError, CreateAFile, JsonFo
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService}
import gitbucket.core.util.Directory.getRepositoryDir
import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo, getContentFromId, getFileList}
import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo, getContentFromId, getFileList, getSummaryMessage}
import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import gitbucket.core.util.Implicits._
@@ -49,19 +49,21 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch))
})
private def getFileInfo(git: Git, revision: String, pathStr: String, ignoreCase: Boolean): Option[FileInfo] = {
val (dirName, fileName) = pathStr.lastIndexOf('/') match {
case -1 =>
(".", pathStr)
case n =>
(pathStr.take(n), pathStr.drop(n + 1))
}
if (ignoreCase) {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.toLowerCase.equals(fileName.toLowerCase))
} else {
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
.find(_.name.equals(fileName))
private def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
getPathObjectId(git, pathStr, revCommit).map { objectId =>
FileInfo(
id = objectId,
isDirectory = false,
name = pathStr.split("/").last,
path = pathStr.split("/").dropRight(1).mkString("/"),
message = getSummaryMessage(revCommit.getFullMessage, revCommit.getShortMessage),
commitId = revCommit.getName,
time = revCommit.getAuthorIdent.getWhen,
author = revCommit.getAuthorIdent.getName,
mailAddress = revCommit.getAuthorIdent.getEmailAddress,
linkUrl = None
)
}
}
@@ -74,16 +76,17 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
val fileList = getFileList(git, refStr, path, maxFiles = context.settings.repositoryViewer.maxFiles)
if (fileList.isEmpty) { // file or NotFound
getFileInfo(git, refStr, path, ignoreCase)
.flatMap { f =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(refStr))
getPathObjectId(git, path, revCommit)
.flatMap { objectId =>
val largeFile = params.get("large_file").exists(s => s.equals("true"))
val content = getContentFromId(git, f.id, largeFile)
val content = getContentFromId(git, objectId, largeFile)
request.getHeader("Accept") match {
case "application/vnd.github.v3.raw" => {
contentType = "application/vnd.github.v3.raw"
content
}
case "application/vnd.github.v3.html" if isRenderable(f.name) => {
case "application/vnd.github.v3.html" if isRenderable(path) => {
contentType = "application/vnd.github.v3.html"
content.map { c =>
List(
@@ -114,11 +117,12 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
}
}
case _ =>
Some(JsonFormat(ApiContents(f, RepositoryName(repository), content)))
getFileInfo(git, refStr, path).map { f =>
JsonFormat(ApiContents(f, RepositoryName(repository), content))
}
}
}
.getOrElse(NotFound())
} else { // directory
JsonFormat(fileList.map { f =>
ApiContents(f, RepositoryName(repository), None)
@@ -148,16 +152,14 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
val path = paths.take(paths.size - 1).toList.mkString("/")
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
git =>
val fileInfo = getFileInfo(git, commit, path, false)
fileInfo match {
getFileInfo(git, commit, path) match {
case Some(f) if !data.sha.contains(f.id.getName) =>
ApiError(
"The blob SHA is not matched.",
Some("https://docs.github.com/en/rest/reference/repos#create-or-update-file-contents")
)
case _ =>
commitFile(
val (commitId, blobId) = commitFile(
repository,
branch,
path,
@@ -170,12 +172,12 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
data.committer.map(_.name).getOrElse(loginAccount.fullName),
data.committer.map(_.email).getOrElse(loginAccount.mailAddress),
context.settings
) match {
case Left(error) =>
ApiError(s"Failed to commit a file: ${error}", None)
case Right((_, None)) =>
)
blobId match {
case None =>
ApiError("Failed to commit a file.", None)
case Right((commitId, Some(blobId))) =>
case Some(blobId) =>
Map(
"content" -> ApiContents(
"file",

View File

@@ -16,7 +16,6 @@ import scala.util.Using
trait ApiRepositoryControllerBase extends ControllerBase {
self: RepositoryService
with ApiGitReferenceControllerBase
with RepositoryCreationService
with AccountService
with OwnerAuthenticator
@@ -185,11 +184,9 @@ trait ApiRepositoryControllerBase extends ControllerBase {
* https://docs.github.com/en/rest/reference/repos#list-repository-tags
*/
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
JsonFormat(
self.getRef("tags", repository)
)
}
JsonFormat(
repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.id))
)
})
/*

View File

@@ -1,191 +0,0 @@
package gitbucket.core.model
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.StringUtil
import gitbucket.core.view.helpers
import org.scalatra.i18n.Messages
trait CustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
lazy val CustomFields = TableQuery[CustomFields]
class CustomFields(tag: Tag) extends Table[CustomField](tag, "CUSTOM_FIELD") with BasicTemplate {
val fieldId = column[Int]("FIELD_ID", O AutoInc)
val fieldName = column[String]("FIELD_NAME")
val fieldType = column[String]("FIELD_TYPE")
val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES")
val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS")
def * =
(userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests)
.<>(CustomField.tupled, CustomField.unapply)
def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) =
(this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind)
}
}
case class CustomField(
userName: String,
repositoryName: String,
fieldId: Int = 0,
fieldName: String,
fieldType: String, // long, double, string, or date
enableForIssues: Boolean,
enableForPullRequests: Boolean
)
trait CustomFieldBehavior {
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit conext: Context): String
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String
def validate(name: String, value: String, messages: Messages): Option[String]
}
object CustomFieldBehavior {
def validate(field: CustomField, value: String, messages: Messages): Option[String] = {
if (value.isEmpty) None
else {
CustomFieldBehavior(field.fieldType).flatMap { behavior =>
behavior.validate(field.fieldName, value, messages)
}
}
}
def apply(fieldType: String): Option[CustomFieldBehavior] = {
fieldType match {
case "long" => Some(LongFieldBehavior)
case "double" => Some(DoubleFieldBehavior)
case "string" => Some(StringFieldBehavior)
case "date" => Some(DateFieldBehavior)
case _ => None
}
}
case object LongFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toLong
None
} catch {
case _: NumberFormatException => Some(messages("error.number").format(name))
}
}
}
case object DoubleFieldBehavior extends TextFieldBehavior {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
value.toDouble
None
} catch {
case _: NumberFormatException => Some(messages("error.number").format(name))
}
}
}
case object StringFieldBehavior extends TextFieldBehavior
case object DateFieldBehavior extends TextFieldBehavior {
private val pattern = "yyyy-MM-dd"
override protected val fieldType: String = "date"
override def validate(name: String, value: String, messages: Messages): Option[String] = {
try {
new java.text.SimpleDateFormat(pattern).parse(value)
None
} catch {
case _: java.text.ParseException =>
Some(messages("error.datePattern").format(name, pattern))
}
}
}
trait TextFieldBehavior extends CustomFieldBehavior {
protected val fieldType = "text"
def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = {
val sb = new StringBuilder
sb.append(
s"""<input type="$fieldType" class="form-control input-sm" id="custom-field-$fieldId" name="custom-field-$fieldId" data-field-id="$fieldId" style="width: 120px;"/>"""
)
sb.append(s"""<script>
|$$('#custom-field-$fieldId').focusout(function(){
| const $$this = $$(this);
| const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() },
| function(data){
| if (data != '') {
| $$('#custom-field-$fieldId-error').text(data);
| } else {
| $$('#custom-field-$fieldId-error').text('');
| }
| }
| );
|});
|</script>
|""".stripMargin)
sb.toString()
}
def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)(
implicit context: Context
): String = {
val sb = new StringBuilder
sb.append(
s"""<span id="custom-field-$fieldId-label" class="custom-field-label">${StringUtil
.escapeHtml(value)}</span>""".stripMargin
)
if (editable) {
sb.append(
s"""<input type="$fieldType" id="custom-field-$fieldId-editor" class="form-control input-sm custom-field-editor" data-field-id="$fieldId" style="width: 120px; display: none;"/>"""
)
sb.append(s"""<script>
|$$('#custom-field-$fieldId-label').click(function(){
| const $$this = $$(this);
| $$this.hide();
| $$this.next().val($$this.text()).show().focus();
|});
|
|$$('#custom-field-$fieldId-editor').focusout(function(){
| const $$this = $$(this);
| const fieldId = $$this.data('field-id');
| $$.post('${helpers.url(repository)}/issues/customfield_validation/' + fieldId,
| { value: $$this.val() },
| function(data){
| if (data != '') {
| $$('#custom-field-$fieldId-error').text(data);
| } else {
| $$('#custom-field-$fieldId-error').text('');
| $$.post('${helpers.url(repository)}/issues/$issueId/customfield/' + fieldId,
| { value: $$this.val() },
| function(data){
| $$this.hide();
| $$this.prev().text(data).show();
| }
| );
| }
| }
| );
|});
|
|// ESC key handling in text field
|$$('#custom-field-$fieldId-editor').keyup(function(e){
| if (e.keyCode == 27) {
| const $$this = $$(this);
| $$this.hide();
| $$this.prev().show();
| }
| if (e.keyCode == 13) {
| $$('#custom-field-$fieldId-editor').blur();
| }
|});
|</script>
|""".stripMargin)
}
sb.toString()
}
def validate(name: String, value: String, messages: Messages): Option[String] = None
}
}

View File

@@ -27,6 +27,7 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
with MilestoneTemplate
with PriorityTemplate {
val openedUserName = column[String]("OPENED_USER_NAME")
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
val title = column[String]("TITLE")
val content = column[String]("CONTENT")
val closed = column[Boolean]("CLOSED")
@@ -41,6 +42,7 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
openedUserName,
milestoneId.?,
priorityId.?,
assignedUserName.?,
title,
content.?,
closed,
@@ -60,6 +62,7 @@ case class Issue(
openedUserName: String,
milestoneId: Option[Int],
priorityId: Option[Int],
assignedUserName: Option[String],
title: String,
content: Option[String],
closed: Boolean,

View File

@@ -1,26 +0,0 @@
package gitbucket.core.model
trait IssueAssigneeComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
lazy val IssueAssignees = TableQuery[IssueAssignees]
class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate {
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
def * =
(userName, repositoryName, issueId, assigneeUserName)
.<>(IssueAssignee.tupled, IssueAssignee.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = {
byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind
}
}
}
case class IssueAssignee(
userName: String,
repositoryName: String,
issueId: Int,
assigneeUserName: String
)

View File

@@ -1,31 +0,0 @@
package gitbucket.core.model
trait IssueCustomFieldComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
lazy val IssueCustomFields = TableQuery[IssueCustomFields]
class IssueCustomFields(tag: Tag) extends Table[IssueCustomField](tag, "ISSUE_CUSTOM_FIELD") {
val userName = column[String]("USER_NAME", O.PrimaryKey)
val repositoryName = column[String]("REPOSITORY_NAME", O.PrimaryKey)
val issueId = column[Int]("ISSUE_ID", O.PrimaryKey)
val fieldId = column[Int]("FIELD_ID", O.PrimaryKey)
val value = column[String]("VALUE")
def * =
(userName, repositoryName, issueId, fieldId, value)
.<>(IssueCustomField.tupled, IssueCustomField.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = {
this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind
}
}
}
case class IssueCustomField(
userName: String,
repositoryName: String,
issueId: Int,
fieldId: Int,
value: String
)

View File

@@ -73,8 +73,5 @@ trait CoreProfile
with ReleaseAssetComponent
with AccountExtraMailAddressComponent
with AccountPreferenceComponent
with CustomFieldComponent
with IssueCustomFieldComponent
with IssueAssigneeComponent
object Profile extends CoreProfile

View File

@@ -1,15 +1,14 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import io.github.gitbucket.solidbase.model.Version
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command
import play.twirl.api.Html
import scala.util.Using
/**
@@ -324,7 +323,7 @@ abstract class Plugin {
/**
* Override to add ssh command providers.
*/
val sshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] = Nil
val sshCommandProviders: Seq[PartialFunction[String, Command]] = Nil
/**
* Override to add ssh command providers.
@@ -333,7 +332,7 @@ abstract class Plugin {
registry: PluginRegistry,
context: ServletContext,
settings: SystemSettings
): Seq[PartialFunction[String, ChannelSession => Command]] = Nil
): Seq[PartialFunction[String, Command]] = Nil
/**
* This method is invoked in initialization of plugin system.

View File

@@ -6,6 +6,7 @@ import java.nio.file.{Files, Paths, StandardWatchEventKinds}
import java.util.Base64
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext
import com.github.zafarkhaja.semver.Version
import gitbucket.core.controller.{Context, ControllerBase}
@@ -20,7 +21,6 @@ import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.apache.commons.io.FileUtils
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.server.command.Command
import org.slf4j.LoggerFactory
import play.twirl.api.Html
@@ -58,7 +58,7 @@ class PluginRegistry {
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider())
suggestionProviders.add(new IssueSuggestionProvider())
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, ChannelSession => Command]]()
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
@@ -177,11 +177,10 @@ class PluginRegistry {
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, ChannelSession => Command]): Unit =
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, Command]): Unit =
sshCommandProviders.add(sshCommandProvider)
def getSshCommandProviders: Seq[PartialFunction[String, ChannelSession => Command]] =
sshCommandProviders.asScala.toSeq
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq
}
/**

View File

@@ -7,14 +7,9 @@ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.dateColumnType
import gitbucket.core.util.{LDAPUtil, StringUtil}
import StringUtil._
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
import com.nimbusds.jose.crypto.{MACSigner, MACVerifier}
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings
import java.security.SecureRandom
trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService])
@@ -65,28 +60,28 @@ trait AccountService {
case Right(ldapUserInfo) => {
// Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) =>
if (x.isRemoved) {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
case Some(x) if (!x.isRemoved) => {
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else {
if (settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else {
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
}
getAccountByUserName(ldapUserInfo.userName)
updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
}
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if (x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
}
case None =>
getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
case Some(x) =>
if (x.isRemoved) {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
} else {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if (!x.isRemoved) => {
updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName)
}
case Some(x) if (x.isRemoved) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password)
}
case None => {
createAccount(
ldapUserInfo.userName,
@@ -342,33 +337,6 @@ trait AccountService {
}
}
def generateResetPasswordToken(mailAddress: String): String = {
val claimsSet = new JWTClaimsSet.Builder()
.claim("mailAddress", mailAddress)
.expirationTime(new java.util.Date(System.currentTimeMillis() + 10 * 1000))
.build()
val signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet)
signedJWT.sign(new MACSigner(AccountService.jwtSecretKey))
signedJWT.serialize()
}
def decodeResetPasswordToken(token: String): Option[String] = {
try {
val signedJWT = SignedJWT.parse(token)
val verifier = new MACVerifier(AccountService.jwtSecretKey)
if (signedJWT.verify(verifier) && new java.util.Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
Some(signedJWT.getPayload.toJSONObject.get("mailAddress").toString)
} else None
} catch {
case _: Exception => None
}
}
}
object AccountService extends AccountService {
// 256-bit key for HS256 which must be pre-shared
val jwtSecretKey = new Array[Byte](32)
new SecureRandom().nextBytes(jwtSecretKey)
}
object AccountService extends AccountService

View File

@@ -1,94 +0,0 @@
package gitbucket.core.service
import gitbucket.core.model.{CustomField, IssueCustomField}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
trait CustomFieldsService {
def getCustomFields(owner: String, repository: String)(implicit s: Session): List[CustomField] =
CustomFields.filter(_.byRepository(owner, repository)).sortBy(_.fieldId asc).list
def getCustomFieldsWithValue(owner: String, repository: String, issueId: Int)(
implicit s: Session
): List[(CustomField, Option[IssueCustomField])] = {
CustomFields
.filter(_.byRepository(owner, repository))
.joinLeft(IssueCustomFields)
.on { case (t1, t2) => t1.fieldId === t2.fieldId && t2.issueId === issueId.bind }
.sortBy { case (t1, t2) => t1.fieldId }
.list
}
def getCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Option[CustomField] =
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).firstOption
def createCustomField(
owner: String,
repository: String,
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(implicit s: Session): Int = {
CustomFields returning CustomFields.map(_.fieldId) insert CustomField(
userName = owner,
repositoryName = repository,
fieldName = fieldName,
fieldType = fieldType,
enableForIssues = enableForIssues,
enableForPullRequests = enableForPullRequests
)
}
def updateCustomField(
owner: String,
repository: String,
fieldId: Int,
fieldName: String,
fieldType: String,
enableForIssues: Boolean,
enableForPullRequests: Boolean
)(
implicit s: Session
): Unit =
CustomFields
.filter(_.byPrimaryKey(owner, repository, fieldId))
.map(t => (t.fieldName, t.fieldType, t.enableForIssues, t.enableForPullRequests))
.update((fieldName, fieldType, enableForIssues, enableForPullRequests))
def deleteCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Unit = {
IssueCustomFields
.filter(t => t.userName === owner.bind && t.repositoryName === repository.bind && t.fieldId === fieldId.bind)
.delete
CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).delete
}
def getCustomFieldValues(
userName: String,
repositoryName: String,
issueId: Int,
)(implicit s: Session): List[IssueCustomField] = {
IssueCustomFields
.filter(t => t.userName === userName && t.repositoryName === repositoryName.bind && t.issueId === issueId.bind)
.list
}
def insertOrUpdateCustomFieldValue(
field: CustomField,
userName: String,
repositoryName: String,
issueId: Int,
value: String
)(implicit s: Session): Unit = {
IssueCustomFields.insertOrUpdate(
IssueCustomField(
userName = userName,
repositoryName = repositoryName,
issueId = issueId,
fieldId = field.fieldId,
value = value
)
)
}
}

View File

@@ -165,7 +165,7 @@ trait HandleCommentService {
content match {
case Some(content) =>
// Update comment
val _commentId = Some(updateComment(owner, name, issue.issueId, commentId.toInt, content))
val _commentId = Some(updateComment(issue.issueId, commentId.toInt, content))
// Record comment activity
val commentInfo = if (issue.isPullRequest) {
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)

View File

@@ -16,7 +16,7 @@ trait IssueCreationService {
repository: RepositoryInfo,
title: String,
body: Option[String],
assignees: Seq[String],
assignee: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Seq[String],
@@ -35,19 +35,16 @@ trait IssueCreationService {
userName,
title,
body,
if (manageable) assignee else None,
if (manageable) milestoneId else None,
if (manageable) priorityId else None
)
val issue: Issue = getIssue(owner, name, issueId.toString).get
// insert labels
if (manageable) {
// insert assignees
assignees.foreach { assignee =>
registerIssueAssignee(owner, name, issueId, assignee)
}
// insert labels
val labels = getLabels(owner, name)
labelNames.foreach { labelName =>
labelNames.map { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
}

View File

@@ -5,17 +5,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.controller.Context
import gitbucket.core.model.{
Account,
Issue,
IssueAssignee,
IssueComment,
IssueLabel,
Label,
PullRequest,
Repository,
Role
}
import gitbucket.core.model.{Account, Issue, IssueComment, IssueLabel, Label, PullRequest, Repository, Role}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile._
import gitbucket.core.model.Profile.profile.blockingApi._
@@ -128,7 +118,8 @@ trait IssuesService {
def countIssueGroupByLabels(
owner: String,
repository: String,
condition: IssueSearchCondition
condition: IssueSearchCondition,
filterUser: Map[String, String]
)(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
@@ -147,7 +138,7 @@ trait IssuesService {
t3.labelName
}
.map {
case (labelName, t) =>
case labelName ~ t =>
labelName -> t.length
}
.list
@@ -165,7 +156,8 @@ trait IssuesService {
def countIssueGroupByPriorities(
owner: String,
repository: String,
condition: IssueSearchCondition
condition: IssueSearchCondition,
filterUser: Map[String, String]
)(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
@@ -179,7 +171,7 @@ trait IssuesService {
t2.priorityName
}
.map {
case (priorityName, t) =>
case priorityName ~ t =>
priorityName -> t.length
}
.list
@@ -215,11 +207,9 @@ trait IssuesService {
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
.joinLeft(PullRequests)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t1.byIssue(t8.userName, t8.repositoryName, t8.issueId) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map {
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 =>
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 =>
(
t1,
t2.commentCount,
@@ -228,8 +218,7 @@ trait IssuesService {
t4.map(_.color),
t5.map(_.title),
t6.map(_.priorityName),
t7.map(_.commitIdTo),
t8.map(_.assigneeUserName)
t7.map(_.commitIdTo)
)
}
.list
@@ -239,7 +228,7 @@ trait IssuesService {
result.map { issues =>
issues.head match {
case (issue, commentCount, _, _, _, milestone, priority, commitId, _) =>
case (issue, commentCount, _, _, _, milestone, priority, commitId) =>
IssueInfo(
issue,
issues.flatMap { t =>
@@ -248,39 +237,27 @@ trait IssuesService {
milestone,
priority,
commentCount,
commitId,
issues.flatMap(_._9)
commitId
)
}
} toList
}
/** for api
* @return (issue, issueUser, Seq(assigneeUsers))
* @return (issue, issueUser, assignedUser)
*/
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session
): List[(Issue, Account, List[Account])] = {
): List[(Issue, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
.join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t4.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => (t1, t3, t5) }
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.userName === t1.assignedUserName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 => (t1, t3, t4) }
.list
.groupBy {
case (issue, account, _) =>
(issue, account)
}
.map {
case (_, values) =>
(values.head._1, values.head._2, values.flatMap(_._3))
}
.toList
}
/** for api
@@ -288,7 +265,7 @@ trait IssuesService {
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session
): List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] = {
): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
.join(PullRequests)
@@ -299,30 +276,11 @@ trait IssuesService {
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t8.userName === t7.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => (t1, t5, t2.commentCount, t3, t4, t6, t8) }
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list
.groupBy {
case (issue, openedUser, commentCount, pullRequest, repository, account, assignedUser) =>
(issue, openedUser, commentCount, pullRequest, repository, account)
}
.map {
case (_, values) =>
(
values.head._1,
values.head._2,
values.head._3,
values.head._4,
values.head._5,
values.head._6,
values.flatMap(_._7)
)
}
.toList
}
private def searchIssueQueryBase(
@@ -389,7 +347,7 @@ trait IssuesService {
case _ => t1.closed === true || t1.closed === false
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
//.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(searchOption match {
case IssueSearchOption.Issues => t1.pullRequest === false
@@ -413,13 +371,7 @@ trait IssuesService {
condition.priority.flatten.isDefined
)
// Assignee filter
.&&(
IssueAssignees filter { a =>
a.byIssue(t1.userName, t1.repositoryName, t1.issueId) &&
a.assigneeUserName === condition.assigned.get.get.bind
} exists,
condition.assigned.flatten.isDefined
)
.&&(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined)
// Label filter
.&&(
IssueLabels filter { t2 =>
@@ -444,9 +396,7 @@ trait IssuesService {
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
// Mentioned filter
.&&(
(t1.openedUserName === condition.mentioned.get.bind) || (IssueAssignees filter { t1 =>
t1.byIssue(t1.userName, t1.repositoryName, t1.issueId) && t1.assigneeUserName === condition.mentioned.get.bind
} exists) ||
(t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
(IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists),
@@ -460,6 +410,7 @@ trait IssuesService {
loginUser: String,
title: String,
content: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
isPullRequest: Boolean = false
@@ -476,6 +427,7 @@ trait IssuesService {
loginUser,
milestoneId,
priorityId,
assignedUserName,
title,
content,
false,
@@ -557,7 +509,7 @@ trait IssuesService {
content: String,
action: String
)(implicit s: Session): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner,
repositoryName = repository,
@@ -590,91 +542,35 @@ trait IssuesService {
.update(true)
}
def getIssueAssignees(owner: String, repository: String, issueId: Int)(
implicit s: Session
): List[IssueAssignee] = {
IssueAssignees.filter(_.byIssue(owner, repository, issueId)).sortBy(_.assigneeUserName).list
}
def registerIssueAssignee(
def updateAssignedUserName(
owner: String,
repository: String,
issueId: Int,
assigneeUserName: String,
assignedUserName: Option[String],
insertComment: Boolean = false
)(
implicit context: Context,
s: Session
): Int = {
)(implicit context: Context, s: Session): Int = {
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName
val assigned = assignedUserName
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "add_assignee",
action = "assign",
commentedUserName = assigner.getOrElse("Unknown user"),
content = assigneeUserName,
content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""",
registeredDate = currentDate,
updatedDate = currentDate
)
}
for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, Some(assigneeUserName), None))
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned))
}
IssueAssignees insert IssueAssignee(owner, repository, issueId, assigneeUserName)
}
def deleteIssueAssignee(
owner: String,
repository: String,
issueId: Int,
assigneeUserName: String,
insertComment: Boolean = false
)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assignee",
commentedUserName = assigner.getOrElse("Unknown user"),
content = assigneeUserName,
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byPrimaryKey(owner, repository, issueId, assigneeUserName)) delete
}
def deleteAllIssueAssignees(owner: String, repository: String, issueId: Int, insertComment: Boolean = false)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assign",
commentedUserName = assigner.getOrElse("Unknown user"),
content = "All assignees",
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byIssue(owner, repository, issueId)) delete
Issues
.filter(_.byPrimaryKey(owner, repository, issueId))
.map(t => (t.assignedUserName ?, t.updatedDate))
.update(assignedUserName, currentDate)
}
def updateMilestoneId(
@@ -739,10 +635,8 @@ trait IssuesService {
.update(priorityId, currentDate)
}
def updateComment(owner: String, repository: String, issueId: Int, commentId: Int, content: String)(
implicit s: Session
): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
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)
}
@@ -750,13 +644,13 @@ trait IssuesService {
implicit context: Context,
s: Session
): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).first match {
case c if c.action == "reopen_comment" =>
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
case Some(c) if c.action == "reopen_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
case c if c.action == "close_comment" =>
case Some(c) if c.action == "close_comment" =>
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
case _ =>
case Some(_) =>
IssueComments.filter(_.byPrimaryKey(commentId)).delete
IssueComments insert IssueComment(
userName = owner,
@@ -1087,8 +981,7 @@ object IssuesService {
milestone: Option[String],
priority: Option[String],
commentCount: Int,
commitId: Option[String],
assignees: Seq[String]
commitId: Option[String]
)
}

View File

@@ -579,7 +579,7 @@ trait PullRequestService {
case (oldGit, newGit) =>
if (originRepository.branchList.contains(originId)) {
val forkedId2 =
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
val originId2 = JGitUtil.getForkedCommitId(
oldGit,
@@ -596,9 +596,9 @@ trait PullRequestService {
} else {
val originId2 =
originRepository.tags.collectFirst { case x if x.name == originId => x.commitId }.getOrElse(originId)
originRepository.tags.collectFirst { case x if x.name == originId => x.id }.getOrElse(originId)
val forkedId2 =
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.commitId }.getOrElse(forkedId)
forkedRepository.tags.collectFirst { case x if x.name == forkedId => x.id }.getOrElse(forkedId)
(Option(oldGit.getRepository.resolve(originId2)), Option(newGit.getRepository.resolve(forkedId2)))
}

View File

@@ -36,10 +36,10 @@ trait RepositoryCommitFileService {
settings: SystemSettings
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit
)(implicit s: Session, c: JsonFormat.Context): Either[String, ObjectId] = {
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
_createFiles(repository, branch, message, loginAccount, loginAccount.fullName, loginAccount.mailAddress, settings)(
f
).map(_._1)
)._1
}
/**
@@ -58,7 +58,7 @@ trait RepositoryCommitFileService {
commit: String,
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
commitFile(
repository,
branch,
@@ -92,7 +92,7 @@ trait RepositoryCommitFileService {
committerName: String,
committerMailAddress: String,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, Option[ObjectId]) = {
val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}"
@@ -141,7 +141,7 @@ trait RepositoryCommitFileService {
settings: SystemSettings
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
)(implicit s: Session, c: JsonFormat.Context): (ObjectId, R) = {
LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
@@ -177,11 +177,11 @@ trait RepositoryCommitFileService {
error match {
case Some(error) =>
// commit is rejected
// TODO Notify commit failure to edited user
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(headTip)
refUpdate.setForceUpdate(true)
refUpdate.update()
Left(error)
case None =>
// update refs
@@ -242,8 +242,8 @@ trait RepositoryCommitFileService {
)
}
}
Right((commitId, result))
}
(commitId, result)
}
}
}

View File

@@ -12,7 +12,6 @@ import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{Repository => _}
import scala.util.Using
trait RepositoryService {
@@ -342,7 +341,10 @@ trait RepositoryService {
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getOpenMilestones(repository.userName, repository.repositoryName),
getOpenMilestones(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
),
getRepositoryManagers(repository.userName, repository.repositoryName)
)
}
@@ -833,10 +835,12 @@ object RepositoryService {
def httpUrl(owner: String, name: String)(implicit context: Context): String =
s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
context.settings.sshUrl(owner, name)
if (context.settings.ssh.enabled) {
context.settings.sshAddress.map { x =>
s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git"
}
} else None
def openRepoUrl(openUrl: String)(implicit context: Context): String =
s"github-${context.platform}://openRepo/${openUrl}"

View File

@@ -4,10 +4,9 @@ import javax.servlet.http.HttpServletRequest
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
import gitbucket.core.service.SystemSettingsService.{getOptionValue, _}
import gitbucket.core.service.SystemSettingsService._
import gitbucket.core.util.ConfigUtil._
import gitbucket.core.util.Directory._
import scala.util.Using
trait SystemSettingsService {
@@ -18,27 +17,20 @@ trait SystemSettingsService {
val props = new java.util.Properties()
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
settings.information.foreach(x => props.setProperty(Information, x))
props.setProperty(AllowAccountRegistration, settings.basicBehavior.allowAccountRegistration.toString)
props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString)
props.setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.basicBehavior.isCreateRepoOptionPublic.toString)
props.setProperty(RepositoryOperationCreate, settings.basicBehavior.repositoryOperation.create.toString)
props.setProperty(RepositoryOperationDelete, settings.basicBehavior.repositoryOperation.delete.toString)
props.setProperty(RepositoryOperationRename, settings.basicBehavior.repositoryOperation.rename.toString)
props.setProperty(RepositoryOperationTransfer, settings.basicBehavior.repositoryOperation.transfer.toString)
props.setProperty(RepositoryOperationFork, settings.basicBehavior.repositoryOperation.fork.toString)
props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
props.setProperty(Notification, settings.basicBehavior.notification.toString)
props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString)
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString)
props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString)
props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString)
props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString)
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
settings.ssh.bindAddress.foreach { bindAddress =>
props.setProperty(SshBindAddressHost, bindAddress.host.trim())
props.setProperty(SshBindAddressPort, bindAddress.port.toString)
}
settings.ssh.publicAddress.foreach { publicAddress =>
props.setProperty(SshPublicAddressHost, publicAddress.host.trim())
props.setProperty(SshPublicAddressPort, publicAddress.port.toString)
}
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.ssh.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
props.setProperty(UseSMTP, settings.useSMTP.toString)
if (settings.useSMTP) {
settings.smtp.foreach { smtp =>
@@ -103,44 +95,26 @@ trait SystemSettingsService {
props.load(in)
}
}
loadSystemSettings(props)
}
def loadSystemSettings(props: java.util.Properties): SystemSettings = {
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue(props, Information, None),
BasicBehavior(
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowResetPassword, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
RepositoryOperation(
create = getValue(props, RepositoryOperationCreate, true),
delete = getValue(props, RepositoryOperationDelete, true),
rename = getValue(props, RepositoryOperationRename, true),
transfer = getValue(props, RepositoryOperationTransfer, true),
fork = getValue(props, RepositoryOperationFork, true)
),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false)
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
RepositoryOperation(
create = getValue(props, RepositoryOperationCreate, true),
delete = getValue(props, RepositoryOperationDelete, true),
rename = getValue(props, RepositoryOperationRename, true),
transfer = getValue(props, RepositoryOperationTransfer, true),
fork = getValue(props, RepositoryOperationFork, true)
),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getValue(props, LimitVisibleRepositories, false),
Ssh(
enabled = getValue(props, SshEnabled, false),
bindAddress = {
// try the new-style configuration first
getOptionValue[String](props, SshBindAddressHost, None)
.map(h => SshAddress(h, getValue(props, SshBindAddressPort, DefaultSshPort), GenericSshUser))
.orElse(
// otherwise try to get old-style configuration
getOptionValue[String](props, SshHost, None)
.map(_.trim)
.map(h => SshAddress(h, getValue(props, SshPort, DefaultSshPort), GenericSshUser))
)
},
publicAddress = getOptionValue[String](props, SshPublicAddressHost, None)
.map(h => SshAddress(h, getValue(props, SshPublicAddressPort, PublicSshPort), GenericSshUser))
getValue(props, SshEnabled, false),
getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort))
),
getValue(
props,
@@ -208,6 +182,7 @@ trait SystemSettingsService {
)
)
}
}
object SystemSettingsService {
@@ -218,7 +193,13 @@ object SystemSettingsService {
case class SystemSettings(
baseUrl: Option[String],
information: Option[String],
basicBehavior: BasicBehavior,
allowAccountRegistration: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean,
notification: Boolean,
limitVisibleRepositories: Boolean,
ssh: Ssh,
useSMTP: Boolean,
smtp: Option[Smtp],
@@ -233,6 +214,7 @@ object SystemSettingsService {
upload: Upload,
repositoryViewer: RepositoryViewerSettings
) {
def baseUrl(request: HttpServletRequest): String =
baseUrl.getOrElse(parseBaseUrl(request)).stripSuffix("/")
@@ -249,30 +231,13 @@ object SystemSettingsService {
.fold(base)(_ + base.dropWhile(_ != ':'))
}
def sshBindAddress: Option[SshAddress] =
ssh.bindAddress
def sshPublicAddress: Option[SshAddress] =
ssh.publicAddress.orElse(ssh.bindAddress)
def sshUrl: Option[String] =
ssh.getUrl
def sshUrl(owner: String, name: String): Option[String] =
ssh.getUrl(owner: String, name: String)
def sshAddress: Option[SshAddress] =
ssh.sshHost.collect {
case host if ssh.enabled =>
SshAddress(host, ssh.sshPort.getOrElse(DefaultSshPort), "git")
}
}
case class BasicBehavior(
allowAccountRegistration: Boolean,
allowResetPassword: Boolean,
allowAnonymousAccess: Boolean,
isCreateRepoOptionPublic: Boolean,
repositoryOperation: RepositoryOperation,
gravatar: Boolean,
notification: Boolean,
limitVisibleRepositories: Boolean,
)
case class RepositoryOperation(
create: Boolean,
delete: Boolean,
@@ -283,35 +248,9 @@ object SystemSettingsService {
case class Ssh(
enabled: Boolean,
bindAddress: Option[SshAddress],
publicAddress: Option[SshAddress]
) {
def getUrl: Option[String] =
if (enabled) {
publicAddress.map(_.getUrl).orElse(bindAddress.map(_.getUrl))
} else {
None
}
def getUrl(owner: String, name: String): Option[String] =
if (enabled) {
publicAddress
.map(_.getUrl(owner, name))
.orElse(bindAddress.map(_.getUrl(owner, name)))
} else {
None
}
}
object Ssh {
def apply(
enabled: Boolean,
bindAddress: Option[SshAddress],
publicAddress: Option[SshAddress]
): Ssh =
new Ssh(enabled, bindAddress, publicAddress.orElse(bindAddress))
}
sshHost: Option[String],
sshPort: Option[Int]
)
case class Ldap(
host: String,
@@ -357,25 +296,7 @@ object SystemSettingsService {
password: Option[String]
)
case class SshAddress(host: String, port: Int, genericUser: String) {
def isDefaultPort: Boolean =
port == PublicSshPort
def getUrl: String =
if (isDefaultPort) {
s"${genericUser}@${host}"
} else {
s"${genericUser}@${host}:${port}"
}
def getUrl(owner: String, name: String): String =
if (isDefaultPort) {
s"${genericUser}@${host}:${owner}/${name}.git"
} else {
s"ssh://${genericUser}@${host}:${port}/${owner}/${name}.git"
}
}
case class SshAddress(host: String, port: Int, genericUser: String)
case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])
@@ -383,8 +304,6 @@ object SystemSettingsService {
case class RepositoryViewerSettings(maxFiles: Int)
val GenericSshUser = "git"
val PublicSshPort = 22
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -392,7 +311,6 @@ object SystemSettingsService {
private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration"
private val AllowResetPassword = "allow_reset_password"
private val AllowAnonymousAccess = "allow_anonymous_access"
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val RepositoryOperationCreate = "repository_operation_create"
@@ -407,10 +325,6 @@ object SystemSettingsService {
private val SshEnabled = "ssh"
private val SshHost = "ssh.host"
private val SshPort = "ssh.port"
private val SshBindAddressHost = "ssh.bindAddress.host"
private val SshBindAddressPort = "ssh.bindAddress.port"
private val SshPublicAddressHost = "ssh.publicAddress.host"
private val SshPublicAddressPort = "ssh.publicAddress.port"
private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"

View File

@@ -379,12 +379,8 @@ trait WebHookPullRequestService extends WebHookService {
settings: SystemSettings
)(implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
val users =
getAccountsByUserNames(
Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
Set(sender)
)
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender))
for {
repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.openedUserName)
@@ -397,7 +393,7 @@ trait WebHookPullRequestService extends WebHookService {
issue,
RepositoryName(repository),
ApiUser(issueUser),
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
getApiMilestone(repository, issue.milestoneId getOrElse (0))
@@ -419,15 +415,16 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
for {
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
assignees = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName),
Set(sender)
)
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
@@ -436,7 +433,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action,
issue = issue,
issueUser = issueUser,
assignees = assigneeUsers,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -484,8 +481,8 @@ trait WebHookPullRequestService extends WebHookService {
requestRepository.name,
requestBranch
)
assignees = getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId).flatMap { x =>
getAccountByUserName(x.assigneeUserName, false)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
@@ -495,7 +492,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action,
issue = issue,
issueUser = issueUser,
assignees = assignees,
assignee = assignee,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -525,17 +522,15 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
)(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
val assignees = getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
val users =
getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(sender)
)
getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
@@ -545,7 +540,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
assignees = assigneeUsers,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -574,17 +569,14 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
for {
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
users = getAccountsByUserNames(
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ assignees.map(
_.assigneeUserName
),
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ issue.assignedUserName,
Set(sender)
)
issueUser <- users.get(issue.openedUserName)
repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.commentedUserName)
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
assignedUser = issue.assignedUserName.flatMap(users.get(_))
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
} yield {
@@ -595,7 +587,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
commentUser = commenter,
repository = repository,
repositoryUser = repoOwner,
assignees = assigneeUsers,
assignedUser = assignedUser,
sender = sender,
labels = labels,
milestone = milestone
@@ -718,7 +710,7 @@ object WebHookService {
action: String,
issue: Issue,
issueUser: Account,
assignees: List[Account],
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -739,7 +731,7 @@ object WebHookService {
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
labels = labels,
assignees = assignees.map(ApiUser.apply),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -770,7 +762,7 @@ object WebHookService {
commentUser: Account,
repository: RepositoryInfo,
repositoryUser: Account,
assignees: List[Account],
assignedUser: Option[Account],
sender: Account,
labels: List[Label],
milestone: Option[ApiMilestone]
@@ -782,7 +774,7 @@ object WebHookService {
issue,
RepositoryName(repository),
ApiUser(issueUser),
assignees.map(ApiUser(_)),
assignedUser.map(ApiUser(_)),
labels.map(ApiLabel(_, RepositoryName(repository))),
milestone
),
@@ -807,7 +799,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
assignees: List[Account],
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -836,7 +828,7 @@ object WebHookService {
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
labels = labels,
assignees = assignees.map(ApiUser.apply),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,

View File

@@ -98,30 +98,29 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
Database() withSession { implicit session =>
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
case Some(repository) => {
val execute =
if (!isUpdating && !repository.repository.isPrivate && settings.basicBehavior.allowAnonymousAccess) {
// Authentication is not required
true
} else {
// Authentication is required
val passed = for {
authorizationHeader <- Option(request.getHeader("Authorization"))
account <- authenticateByHeader(authorizationHeader, settings)
} yield
if (isUpdating) {
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
request.setAttribute(Keys.Request.UserName, account.userName)
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
true
} else false
} else if (repository.repository.isPrivate) {
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
request.setAttribute(Keys.Request.UserName, account.userName)
true
} else false
} else true
passed.getOrElse(false)
}
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
// Authentication is not required
true
} else {
// Authentication is required
val passed = for {
authorizationHeader <- Option(request.getHeader("Authorization"))
account <- authenticateByHeader(authorizationHeader, settings)
} yield
if (isUpdating) {
if (hasDeveloperRole(repository.owner, repository.name, Some(account))) {
request.setAttribute(Keys.Request.UserName, account.userName)
request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}")
true
} else false
} else if (repository.repository.isPrivate) {
if (hasGuestRole(repository.owner, repository.name, Some(account))) {
request.setAttribute(Keys.Request.UserName, account.userName)
true
} else false
} else true
passed.getOrElse(false)
}
if (execute) { () =>
chain.doFilter(request, response)

View File

@@ -3,6 +3,7 @@ package gitbucket.core.servlet
import java.io.File
import java.util
import java.util.Date
import scala.util.Using
import gitbucket.core.api
import gitbucket.core.api.JsonFormat.Context
@@ -52,11 +53,15 @@ import org.json4s.jackson.Serialization._
*/
class GitRepositoryServlet extends GitServlet with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
private implicit val jsonFormats: Formats = gitbucket.core.api.JsonFormat.jsonFormats
override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory())
val root: File = new File(Directory.RepositoryHome)
setRepositoryResolver(new GitBucketRepositoryResolver)
super.init(config)
}
@@ -204,7 +209,9 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshUrl(owner, repository)
val sshUrl = settings.sshAddress.map { x =>
s"${x.genericUser}@${x.host}:${x.port}"
}
if (!repository.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
@@ -416,7 +423,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
pusherAccount,
repositoryInfo,
ownerAccount,
ref = command.getRefName,
ref = branchName,
refType = refType
)
}

View File

@@ -5,31 +5,26 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.{AccountService, DeployKeyService, RepositoryService, SystemSettingsService}
import gitbucket.core.servlet.{CommitLogHook, Database}
import gitbucket.core.util.Directory
import org.apache.sshd.server.{Environment, ExitCallback}
import org.apache.sshd.server.{Environment, ExitCallback, SessionAware}
import org.apache.sshd.server.command.{Command, CommandFactory}
import org.apache.sshd.server.session.{ServerSession, ServerSessionAware}
import org.apache.sshd.server.session.ServerSession
import org.slf4j.LoggerFactory
import java.io.{File, InputStream, OutputStream}
import org.eclipse.jgit.api.Git
import Directory._
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.channel.ChannelSession
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.shell.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
import scala.util.Using
object GitCommand {
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
val DefaultCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegexPort22 = """\Agit-(upload|receive)-pack '/?(.+\.git)'\Z""".r
}
abstract class GitCommand extends Command with ServerSessionAware {
abstract class GitCommand extends Command with SessionAware {
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
@@ -62,12 +57,12 @@ abstract class GitCommand extends Command with ServerSessionAware {
}
}
final override def start(channel: ChannelSession, env: Environment): Unit = {
final override def start(env: Environment): Unit = {
val thread = new Thread(newTask())
thread.start()
}
override def destroy(channel: ChannelSession): Unit = {}
override def destroy(): Unit = {}
override def setExitCallback(callback: ExitCallback): Unit = {
this.callback = callback
@@ -164,7 +159,7 @@ class DefaultGitUploadPack(owner: String, repoName: String)
}
}
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshAddress: SshAddress)
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String])
extends DefaultGitCommand(owner, repoName)
with RepositoryService
with AccountService
@@ -182,8 +177,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
val repository = git.getRepository
val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) {
val hook =
new CommitLogHook(owner, repoName, userName(authType), baseUrl, Some(sshAddress.getUrl(owner, repoName)))
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
@@ -233,10 +227,10 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting)
}
}
class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends CommandFactory {
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(channel: ChannelSession, command: String): Command = {
override def createCommand(command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")
@@ -244,24 +238,19 @@ class GitCommandFactory(baseUrl: String, sshAddress: SshAddress) extends Command
case f if f.isDefinedAt(command) => f(command)
}
pluginCommand.map(_.apply(channel)).getOrElse {
val (simpleRegex, defaultRegex) =
if (sshAddress.isDefaultPort) {
(SimpleCommandRegexPort22, DefaultCommandRegexPort22)
} else {
(SimpleCommandRegex, DefaultCommandRegex)
pluginCommand match {
case Some(x) => x
case None =>
command match {
case SimpleCommandRegex("upload", repoName) if (pluginRepository(repoName)) =>
new PluginGitUploadPack(repoName, routing(repoName))
case SimpleCommandRegex("receive", repoName) if (pluginRepository(repoName)) =>
new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload", owner, repoName) => new DefaultGitUploadPack(owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) =>
new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
case _ => new UnknownCommand(command)
}
command match {
case simpleRegex("upload", repoName) if pluginRepository(repoName) =>
new PluginGitUploadPack(repoName, routing(repoName))
case simpleRegex("receive", repoName) if pluginRepository(repoName) =>
new PluginGitReceivePack(repoName, routing(repoName))
case defaultRegex("upload", owner, repoName) =>
new DefaultGitUploadPack(owner, repoName)
case defaultRegex("receive", owner, repoName) =>
new DefaultGitReceivePack(owner, repoName, baseUrl, sshAddress)
case _ => new UnknownCommand(command)
}
}
}

View File

@@ -1,23 +1,20 @@
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.server.channel.ChannelSession
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback}
import org.apache.sshd.server.command.Command
import org.apache.sshd.server.shell.ShellFactory
import java.io.{InputStream, OutputStream}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
class NoShell(sshAddress: SshAddress) extends ShellFactory {
override def createShell(channel: ChannelSession): Command = new Command() {
class NoShell(sshAddress: SshAddress) extends Factory[Command] {
override def create(): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
private var err: OutputStream = null
private var callback: ExitCallback = null
override def start(channel: ChannelSession, env: Environment): Unit = {
val placeholderAddress = sshAddress.getUrl("OWNER", "REPOSITORY_NAME")
override def start(env: Environment): Unit = {
val message =
"""
| Welcome to
@@ -33,8 +30,8 @@ class NoShell(sshAddress: SshAddress) extends ShellFactory {
|
| Please use:
|
| git clone %s
""".stripMargin.format(placeholderAddress).replace("\n", "\r\n") + "\r\n"
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()
@@ -43,7 +40,7 @@ class NoShell(sshAddress: SshAddress) extends ShellFactory {
callback.onExit(127)
}
override def destroy(channel: ChannelSession): Unit = {}
override def destroy(): Unit = {}
override def setInputStream(in: InputStream): Unit = {
this.in = in

View File

@@ -8,13 +8,13 @@ import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.ssh.PublicKeyAuthenticator.AuthType
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.common.AttributeRepository
import org.apache.sshd.common.AttributeStore
import org.slf4j.LoggerFactory
object PublicKeyAuthenticator {
// put in the ServerSession here to be read by GitCommand later
private val authTypeSessionKey = new AttributeRepository.AttributeKey[AuthType]
private val authTypeSessionKey = new AttributeStore.AttributeKey[AuthType]
def putAuthType(serverSession: ServerSession, authType: AuthType): Unit =
serverSession.setAttribute(authTypeSessionKey, authType)

View File

@@ -1,7 +1,8 @@
package gitbucket.core.ssh
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.atomic.AtomicBoolean
import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.Directory
@@ -10,48 +11,40 @@ import org.slf4j.LoggerFactory
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = new AtomicReference[org.apache.sshd.server.SshServer](null)
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(
bindAddress: SshAddress,
publicAddress: SshAddress,
baseUrl: String
): org.apache.sshd.server.SshServer = {
val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
server.setPort(bindAddress.port)
private def configure(sshAddress: SshAddress, baseUrl: String) = {
server.setPort(sshAddress.port)
val provider = new SimpleGeneratorHostKeyProvider(
java.nio.file.Paths.get(s"${Directory.GitBucketHome}/gitbucket.ser")
)
provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(bindAddress.genericUser))
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setCommandFactory(
new GitCommandFactory(baseUrl, publicAddress)
new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}"))
)
server.setShellFactory(new NoShell(publicAddress))
server
server.setShellFactory(new NoShell(sshAddress))
}
def start(bindAddress: SshAddress, publicAddress: SshAddress, baseUrl: String): Unit = {
this.server.synchronized {
val server = configure(bindAddress, publicAddress, baseUrl)
if (this.server.compareAndSet(null, server)) {
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
def start(sshAddress: SshAddress, baseUrl: String) = {
if (active.compareAndSet(false, true)) {
configure(sshAddress, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
}
def stop(): Unit = {
this.server.synchronized {
val server = this.server.getAndSet(null)
if (server != null) {
server.stop()
logger.info("SSH Server is stopped.")
}
def stop() = {
if (active.compareAndSet(true, false)) {
server.stop(true)
logger.info("SSH Server is stopped.")
}
}
def isActive = active.get
}
/*
@@ -66,14 +59,13 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if (settings.sshBindAddress.isDefined && settings.baseUrl.isEmpty) {
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
logger.error("Could not start SshServer because the baseUrl is not configured.")
}
for {
bindAddress <- settings.sshBindAddress
publicAddress <- settings.sshPublicAddress
sshAddress <- settings.sshAddress
baseUrl <- settings.baseUrl
} SshServer.start(bindAddress, publicAddress, baseUrl)
} SshServer.start(sshAddress, baseUrl)
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {

View File

@@ -38,18 +38,8 @@ object DatabaseConfig {
private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
def url(directory: Option[String]): String = {
val sb = new StringBuilder()
sb.append(dbUrl)
if (dbUrl.startsWith("jdbc:mysql:") && dbUrl.indexOf("permitMysqlScheme") == -1) {
if (dbUrl.indexOf("?") == -1) {
sb.append("?permitMysqlScheme")
} else {
sb.append("&permitMysqlScheme")
}
}
sb.toString().replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
}
def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
lazy val url: String = url(None)
lazy val user: String = getValue("db.user", config.getString)

View File

@@ -22,7 +22,9 @@ object Implicits {
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
JsonFormat.Context(context.baseUrl, context.settings.sshUrl)
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x =>
s"${x.genericUser}@${x.host}:${x.port}"
})
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {

View File

@@ -228,11 +228,10 @@ object JGitUtil {
*
* @param name the tag name
* @param time the tagged date
* @param commitId the commit id
* @param id the commit id
* @param message the message of the tagged commit
* @param objectId the tag object id
*/
case class TagInfo(name: String, time: Date, commitId: String, message: String, objectId: String)
case class TagInfo(name: String, time: Date, id: String, message: String)
/**
* The submodule data
@@ -348,8 +347,7 @@ object JGitUtil {
ref.getName.stripPrefix("refs/tags/"),
revCommit.getCommitterIdent.getWhen,
revCommit.getName,
revCommit.getShortMessage,
ref.getObjectId.getName
revCommit.getShortMessage
)
)
} catch {
@@ -514,7 +512,7 @@ object JGitUtil {
/**
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
val i = fullMessage.trim.indexOf('\n')
val firstLine = if (i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage
if (firstLine.length > shortMessage.length) shortMessage else firstLine

View File

@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
htmlMsg: Option[String] = None,
loginAccount: Option[Account] = None
): Option[HtmlEmail] = {
if (settings.basicBehavior.notification) {
if (settings.notification) {
settings.smtp.map { smtp =>
val email = new HtmlEmail
email.setHostName(smtp.host)

View File

@@ -74,11 +74,6 @@ object StringUtil {
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
/**
* URL encode except '/'.
*/
def encodeRefName(value: String): String = urlEncode(value).replace("%2F", "/")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def isInteger(value: String): Boolean = allCatch opt { value.toInt } map (_ => true) getOrElse (false)
@@ -98,8 +93,8 @@ object StringUtil {
detector.handleData(content, 0, content.length)
detector.dataEnd()
detector.getDetectedCharset match {
case null | "US-ASCII" => "UTF-8"
case e => e
case null => "UTF-8"
case e => e
}
}

View File

@@ -18,7 +18,7 @@ trait AvatarImageProvider { self: RequestCache =>
val src = if (mailAddress.isEmpty) {
// by user name
getAccountByUserNameFromCache(userName).map { account =>
if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else {
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
@@ -29,13 +29,13 @@ trait AvatarImageProvider { self: RequestCache =>
} else {
// by mail address
getAccountByMailAddressFromCache(mailAddress).map { account =>
if (account.image.isEmpty && context.settings.basicBehavior.gravatar) {
if (account.image.isEmpty && context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else {
s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}"""
}
} getOrElse {
if (context.settings.basicBehavior.gravatar) {
if (context.settings.gravatar) {
s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
} else {
s"""${context.path}/_unknown/_avatar"""

View File

@@ -212,19 +212,19 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
Html(
message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
case Some(issue) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${StringUtil.escapeHtml(issue.title)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
case None =>
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
val issue = getIssueFromCache(m.group(1), m.group(2), m.group(3))
if (issue.isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${issue.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
}
}
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
case Some(pullreq) =>
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${StringUtil.escapeHtml(pullreq.title)}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
case None =>
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
val pullreq = getIssueFromCache(m.group(1), m.group(2), m.group(3))
if (pullreq.isDefined) {
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${pullreq.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
} else {
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
}
}
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
@@ -276,7 +276,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/**
* URL encode except '/'.
*/
def encodeRefName(value: String): String = StringUtil.encodeRefName(value)
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
def urlEncode(value: String): String = StringUtil.urlEncode(value)

View File

@@ -1,16 +0,0 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter your email address to reset your password.
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="" value="" style="max-width: 400px;"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
</form>
</div>
</div>
}

View File

@@ -1,11 +0,0 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Password has been updated. <a href="@context.path/signin">Sign-in</a> with new password.
</p>
</div>
</div>
}

View File

@@ -1,17 +0,0 @@
@(token: String)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset/form" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter new password:
<input type="password" name="password" id="password" class="form-control" value="" style="max-width: 400px;"/>
<span id="error-password" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
<input type="hidden" name="token" id="token" value="@token"/>
</form>
</div>
</div>
}

View File

@@ -1,11 +0,0 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Send an email to you. Check your mailbox.
</p>
</div>
</div>
}

View File

@@ -6,7 +6,7 @@
<label class="strong">Services</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" name="basicBehavior.gravatar"@if(context.settings.basicBehavior.gravatar){ checked}/>
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
Use Gravatar for profile images
</label>
</fieldset>
@@ -19,40 +19,22 @@
<label class="checkbox">
<input type="checkbox" id="sshEnabled" name="ssh.enabled"@if(context.settings.ssh.enabled){ checked}/>
Enable SSH access to git repository
<span class="muted normal">(Both SSH bind host and Base URL are required if SSH access is enabled)</span>
<span class="muted normal">(Both SSH host and Base URL are required if SSH access is enabled)</span>
</label>
</fieldset>
<div class="ssh">
<div class="bindAddress">
<div class="form-group">
<label class="control-label col-md-2" for="sshBindHost">SSH bind host</label>
<div class="col-md-10">
<input type="text" id="sshBindHost" name="ssh.bindAddress.host" class="form-control" value="@context.settings.ssh.bindAddress.map(_.host)"/>
<span id="error-ssh_bindAddress_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshBindPort">SSH bind port</label>
<div class="col-md-10">
<input type="text" id="sshBindPort" name="ssh.bindAddress.port" class="form-control" value="@context.settings.ssh.bindAddress.map(_.port)"/>
<span id="error-ssh_bindAddress_port" class="error"></span>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label>
<div class="col-md-10">
<input type="text" id="sshHost" name="ssh.host" class="form-control" value="@context.settings.ssh.sshHost"/>
<span id="error-ssh_host" class="error"></span>
</div>
</div>
<div class="publicAddress">
<div class="form-group">
<label class="control-label col-md-2" for="sshPublicHost">SSH public host</label>
<div class="col-md-10">
<input type="text" id="sshPublicHost" name="ssh.publicAddress.host" class="form-control" value="@context.settings.ssh.publicAddress.map(_.host)"/>
<span id="error-ssh_publicAddress_host" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPublicPort">SSH public port</label>
<div class="col-md-10">
<input type="text" id="sshPublicPort" name="ssh.publicAddress.port" class="form-control" value="@context.settings.ssh.publicAddress.map(_.port)"/>
<span id="error-ssh_publicAddress_port" class="error"></span>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label>
<div class="col-md-10">
<input type="text" id="sshPort" name="ssh.port" class="form-control" value="@context.settings.ssh.sshPort"/>
<span id="error-ssh_port" class="error"></span>
</div>
</div>
</div>
@@ -132,7 +114,7 @@
<label class="strong">Notifications</label>
<fieldset>
<label class="checkbox" for="notification">
<input type="checkbox" id="notification" name="basicBehavior.notification"@if(context.settings.basicBehavior.notification){ checked}/>
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
Send notifications
</label>
</fieldset>

View File

@@ -108,30 +108,15 @@
<label class="strong">Account registration</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.allowAccountRegistration" value="true"@if(context.settings.basicBehavior.allowAccountRegistration){ checked}>
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.allowAccountRegistration" value="false"@if(!context.settings.basicBehavior.allowAccountRegistration){ checked}>
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Reset password -->
<!--====================================================================-->
<hr>
<label class="strong">Reset password</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="true"@if(context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Allow</span> <span class="normal">- Allow users to reset password. (SMTP setting is required)</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="false"@if(!context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Deny</span> <span class="normal">- Doesn't allow users to reset password.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Repository operations -->
<!--====================================================================-->
<hr>
@@ -141,11 +126,11 @@
<label class="control-label col-md-2">Create</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.create" value="true"@if(context.settings.basicBehavior.repositoryOperation.create){ checked}>
<input type="radio" name="repositoryOperation.create" value="true"@if(context.settings.repositoryOperation.create){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.create" value="false"@if(!context.settings.basicBehavior.repositoryOperation.create){ checked}>
<input type="radio" name="repositoryOperation.create" value="false"@if(!context.settings.repositoryOperation.create){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
</label>
</div>
@@ -154,11 +139,11 @@
<label class="control-label col-md-2">Delete</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="true"@if(context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<input type="radio" name="repositoryOperation.delete" value="true"@if(context.settings.repositoryOperation.delete){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="false"@if(!context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<input type="radio" name="repositoryOperation.delete" value="false"@if(!context.settings.repositoryOperation.delete){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
</label>
</div>
@@ -167,11 +152,11 @@
<label class="control-label col-md-2">Rename</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="true"@if(context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<input type="radio" name="repositoryOperation.rename" value="true"@if(context.settings.repositoryOperation.rename){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="false"@if(!context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<input type="radio" name="repositoryOperation.rename" value="false"@if(!context.settings.repositoryOperation.rename){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
</label>
</div>
@@ -180,11 +165,11 @@
<label class="control-label col-md-2">Transfer</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="true"@if(context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<input type="radio" name="repositoryOperation.transfer" value="true"@if(context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="false"@if(!context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<input type="radio" name="repositoryOperation.transfer" value="false"@if(!context.settings.repositoryOperation.transfer){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
</label>
</div>
@@ -193,11 +178,11 @@
<label class="control-label col-md-2">Fork</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="true"@if(context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<input type="radio" name="repositoryOperation.fork" value="true"@if(context.settings.repositoryOperation.fork){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="false"@if(!context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<input type="radio" name="repositoryOperation.fork" value="false"@if(!context.settings.repositoryOperation.fork){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
</label>
</div>
@@ -207,11 +192,11 @@
<label class="strong">Default visibility when creating a new repository</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="true"@if(context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="false"@if(!context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
</label>
</fieldset>
@@ -222,11 +207,11 @@
<label class="strong">Anonymous access</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="true"@if(context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="false"@if(!context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
</label>
</fieldset>
@@ -287,11 +272,11 @@
<label><span class="strong">Show repositories in sidebar</span></label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="false"@if(!context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}>
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="true"@if(context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}>
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
</label>
</fieldset>

View File

@@ -17,7 +17,7 @@
</tr>
</thead>
<tbody>
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
<a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@@ -33,7 +33,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@assignedUserNames.map { userName =>
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){

View File

@@ -24,15 +24,57 @@
case "create_tag" => simpleActivity(activity)
case "delete_tag" => simpleActivity(activity)
case "fork" => simpleActivity(activity)
case "push" => pushActivity(activity)
case "create_wiki" => createWikiActivity(activity)
case "edit_wiki" => editWikiActivity(activity)
case "delete_wiki" => simpleActivity(activity)
case "push" => customActivity(activity){
<div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
<div>...</div>
} else {
if(commit.nonEmpty){
<div>
<a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span>
</div>
}
}
}}
</div>
}
case "create_wiki" => customActivity(activity){
<div class="small activity-message">
Created <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div>
}
case "edit_wiki" => customActivity(activity){
activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) =>
<div class="small activity-message">
Edited <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
<a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
</div>
case Array(pageName) =>
<div class="small activity-message">
Edited <a href={s"${context.path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
</div>
}
}
case "delete_wiki" => simpleActivity(activity)
})
</div>
}
}
@customActivity(activity: gitbucket.core.model.Activity)(additionalInfo: Any) = {
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
@additionalInfo
</div>
}
@simpleActivity(activity: gitbucket.core.model.Activity) = {
<div>
<span class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</span>
@@ -43,66 +85,3 @@
</div>
}
@pushActivity(activity: gitbucket.core.model.Activity) = {
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
<div class="small activity-message">
@activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map { case (commit, i) =>
@if(i == 3){
<div>...</div>
} else {
@if(commit.nonEmpty){
<div>
<a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/commit/@{commit. substring(0, 40)}" class="monospace">@{commit.substring(0, 7)}</a>
<span>@{commit.substring(41)}</span>
</div>
}
}
}
</div>
</div>
}
@createWikiActivity(activity: gitbucket.core.model.Activity) = {
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
<div class="small activity-message">
Created <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{activity.additionalInfo}">@{activity.additionalInfo}</a>.
</div>
</div>
}
@editWikiActivity(activity: gitbucket.core.model.Activity) = {
<div>
<div class="muted small">@gitbucket.core.helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@helpers.avatarLink(activity.activityUserName, 16)
@helpers.activityMessage(activity.message)
</div>
@defining(activity.additionalInfo.get.split(":")){ additionalInfo =>
@if(additionalInfo.length == 2) {
@defining((additionalInfo(0), additionalInfo(1))) { case (pageName, commitId) =>
<div class="small activity-message">
Edited <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@pageName">@pageName</a>.
<a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{pageName}/_compare/@{commitId.substring(0, 7)}^...@{commitId.substring(0, 7)}">View the diff »</a>
</div>
}
}
@if(additionalInfo.length == 1) {
@defining(additionalInfo(0)) { pageName =>
<div class="small activity-message">
Edited <a href="@{context.path}/@{activity.userName}/@{activity.repositoryName}/wiki/@{pageName}">@pageName</a>.
</div>
}
}
}
</div>
}

View File

@@ -40,6 +40,8 @@
</div>
</div>
</div>
<link href="@helpers.assets("/vendors/google-code-prettify/prettify.css")" type="text/css" rel="stylesheet"/>
<script src="@helpers.assets("/vendors/google-code-prettify/prettify.js")"></script>
<script>
$(function(){
@if(elastic){

View File

@@ -200,7 +200,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
added the <code>@comment.content</code> label
add the <code>@comment.content</code> label
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -222,7 +222,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
changed priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
change priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -233,40 +233,18 @@
<span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
changed milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
change milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "assign" => { @* for backward compatibility *@
case "assign" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
changed assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "add_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
assigned <code>@comment.content</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "delete_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
unassigned <code>@comment.content</code></code>
change assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -277,7 +255,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
changed title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
change title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -288,7 +266,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
changed base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
change base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -350,9 +328,9 @@
$(function(){
@if(issue.isDefined){
$('.issue-comment-box i.octicon-pencil').click(function(){
let id = $(this).closest('a').data('comment-id');
let url = '@helpers.url(repository)/issue_comments/_data/' + id;
let $content = $('#commentContent-' + id);
var id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
if(!id){
id = $(this).closest('a').data('issue-id');
@@ -367,7 +345,7 @@ $(function(){
});
$('.issue-comment-box i.octicon-x').click(function(){
if(confirm('Are you sure you want to delete this?')) {
const id = $(this).closest('a').data('comment-id');
var id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){
if(data > 0) {
$('#comment-' + id).remove();
@@ -378,9 +356,9 @@ $(function(){
});
}
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
const id = $(this).closest('a').data('comment-id');
const url = '@helpers.url(repository)/commit_comments/_data/' + id;
const $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
var id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
$.get(url, { dataType : 'html' }, function(data){
$content.empty().html(data);
@@ -391,14 +369,14 @@ $(function(){
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
if(confirm('Are you sure you want to delete this?')) {
const id = $(this).closest('a').data('comment-id');
var id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/commit_comments/delete/' + id,
function(data){
if(data > 0) {
const comment = $('.commit-comment-' + id);
var comment = $('.commit-comment-' + id);
// diff view
const tr = comment.closest('.not-diff');
var tr = comment.closest('.not-diff');
if(tr.length > 0){
if(tr.prev('.not-diff').length == 0){
tr.next('.not-diff:has(.reply-comment)').remove();
@@ -407,7 +385,7 @@ $(function(){
}
// comment list view
const panel = comment.closest('div.panel:has(.commit-comment-box)');
var panel = comment.closest('div.panel:has(.commit-comment-box)');
if(panel.length > 0){
comment.parent('.commit-comment-box').remove();
if(panel.has('.commit-comment-box').length == 0){
@@ -423,7 +401,7 @@ $(function(){
});
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
const $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' },
@@ -443,9 +421,9 @@ $(function(){
);
});
@if(issue.isDefined){
@if(issue.isDefined){
$('#issueContent').on('click', ':checkbox', function(ev){
const checkboxes = $('#issueContent :checkbox');
var checkboxes = $('#issueContent :checkbox');
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' },
function(responseContent){
$.ajax({
@@ -461,7 +439,7 @@ $(function(){
});
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
const $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' },
@@ -477,7 +455,9 @@ $(function(){
}
);
});
}
}
});
</script>
}

View File

@@ -3,7 +3,6 @@
priorities: List[gitbucket.core.model.Priority],
defaultPriority: Option[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label],
customFields: List[gitbucket.core.model.CustomField],
isManageable: Boolean,
content: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@@ -28,37 +27,13 @@
elastic = true
)
<div class="align-right">
<input type="submit" class="btn btn-success" value="Submit new issue" onclick="javascript:return checkCustomFieldErrors();"/>
<input type="submit" class="btn btn-success" value="Submit new issue"/>
</div>
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map(x => (x, 0, 0)),
priorities= priorities,
defaultPriority = defaultPriority,
labels = labels,
customFields = customFields.map((_, None)),
isManageable = isManageable,
repository = repository
)
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository)
</div>
</div>
</form>
<script>
function checkCustomFieldErrors() {
let error = false;
$('.custom-field-error').each(function(i, e) {
if ($(e).text() != '') {
error = true;
}
});
return !error;
}
</script>
}
}

View File

@@ -1,12 +1,10 @@
@(issue: gitbucket.core.model.Issue,
comments: List[gitbucket.core.model.IssueComment],
issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isEditable: Boolean,
isManageable: Boolean,
isCommentManageable: Boolean,
@@ -58,20 +56,7 @@
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(
issue = Some(issue),
comments = comments,
issueLabels = issueLabels,
issueAssignees = issueAssignees,
collaborators = collaborators,
milestones = milestones,
priorities = priorities,
defaultPriority = None,
labels = labels,
customFields = customFields,
isManageable = isManageable,
repository = repository
)
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
</div>
</div>
}

View File

@@ -1,14 +1,11 @@
@import org.json4s.scalap.scalasig.ClassFileParser.field
@(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment],
issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
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],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -128,10 +125,11 @@
@if(isManageable){
<div class="pull-right">
@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>
<a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator">
@gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @collaborator
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@gitbucket.core.helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@helpers.avatar(collaborator, 20) @collaborator
</a>
</li>
}
@@ -140,35 +138,15 @@
}
</div>
<span id="label-assigned">
@issueAssignees.map { asignee =>
<div>@helpers.avatarLink(asignee.assigneeUserName, 20) @helpers.user(asignee.assigneeUserName, styleClass="username strong small")</div>
}
@if(issueAssignees.isEmpty) {
<span class="muted small">No one assigned</span>
@issue.flatMap(_.assignedUserName).map { userName =>
@helpers.avatarLink(userName, 20) @helpers.user(userName, styleClass="username strong small")
}.getOrElse{
<span class="muted small">No one</span>
}
</span>
@if(issue.isEmpty){
<input type="hidden" name="assigneeUserNames" value=""/>
<input type="hidden" name="assignedUserName" value=""/>
}
@customFields.map { case (field, value) =>
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">@field.fieldName</span>
<div class="pull-right">
@gitbucket.core.model.CustomFieldBehavior(field.fieldType).map { behavior =>
@if(issue.nonEmpty) {
@Html(behavior.fieldHtml(repository, issue.get.issueId, field.fieldId, value.map(_.value).getOrElse(""), isManageable))
}
@if(issue.isEmpty) {
@Html(behavior.createHtml(repository, field.fieldId))
}
}
</div>
</div>
<span id="custom-field-@field.fieldId-error" class="error custom-field-error"></span>
}
@issue.map { issue =>
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent =>
@sidebarComponent(issue, repository, context)
@@ -189,7 +167,7 @@
$(function(){
@issue.map { issue =>
$('a.toggle-label').click(function(){
const path = switchToggleOptions($(this));
var path = switchLabel($(this));
$.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path,
{ labelId : $(this).data('label-id') },
function(data){
@@ -200,8 +178,8 @@ $(function(){
});
$('a.milestone').click(function(){
const title = $(this).data('title');
const milestoneId = $(this).data('id');
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@helpers.url(repository)/issues/@issue.issueId/milestone',
{ milestoneId: milestoneId },
function(data){
@@ -211,11 +189,11 @@ $(function(){
});
$('a.priority').click(function(){
const priorityName = $(this).data('name');
const priorityId = $(this).data('id');
const description = $(this).attr('title');
const color = $(this).data('color');
const fontColor = $(this).data('font-color');
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){
@@ -224,26 +202,20 @@ $(function(){
);
});
$('a.toggle-assign').click(function(){
const path = switchToggleOptions($(this));
$.post('@helpers.url(repository)/issues/@issue.issueId/assignee/' + path,
{ assigneeUserName : $(this).data('name') },
function(data){
const assignees = Array();
$('a.toggle-assign').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
displayAssignee(assignees);
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@helpers.url(repository)/issues/@issue.issueId/assign',
{ assignedUserName: userName },
function(){
displayAssignee($this, userName);
}
);
return false;
});
}.getOrElse {
$('a.toggle-label').click(function(){
switchToggleOptions($(this));
const labelNames = Array();
switchLabel($(this));
var labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
labelNames.push($(e).text().trim());
@@ -260,37 +232,32 @@ $(function(){
});
$('a.milestone').click(function(){
const title = $(this).data('title');
const milestoneId = $(this).data('id');
var title = $(this).data('title');
var milestoneId = $(this).data('id');
displayMilestone(title, milestoneId);
$('input[name=milestoneId]').val(milestoneId);
});
$('a.priority').click(function(){
const priorityName = $(this).data('name');
const priorityId = $(this).data('id');
const description = $(this).attr('title');
const color = $(this).data('color');
const fontColor = $(this).data('font-color');
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.toggle-assign').click(function(){
switchToggleOptions($(this));
const assignees = Array();
$('a.toggle-assign').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
$('input[name=assigneeUserNames]').val(assignees.join(','));
displayAssignee(assignees);
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
displayAssignee($this, userName);
$('input[name=assignedUserName]').val(userName);
});
}
function switchToggleOptions($this){
const i = $this.children('i');
function switchLabel($this){
var i = $this.children('i');
if(i.hasClass('octicon-check')){
i.removeClass('octicon-check');
return 'delete';
@@ -332,18 +299,15 @@ $(function(){
}
}
function displayAssignee(assignees){
function displayAssignee($this, userName){
$('a.assign i.octicon-check').removeClass('octicon-check');
if(assignees.length == 0){
$('#label-assigned').html($('<span class="muted small">').text('No one assigned'));
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty();
for (const userName of assignees) {
$('#label-assigned').append($('<div>').append(
$('a.toggle-assign').parent().find("img.avatar-mini[alt='@@" + userName + "']").clone(false),
' ',
$('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName)));
}
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName.toString()) + '] i').addClass('octicon-check');
}
}
});

View File

@@ -206,7 +206,7 @@
</td>
</tr>
}
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(isManageable){
@@ -230,7 +230,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@assignedUserNames.map { userName =>
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){

View File

@@ -62,7 +62,7 @@
</td>
</tr>
}
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId, assignedUserNames), status) =>
@issues.map { case (IssueInfo(issue, labels, milestone, priority, commentCount, commitId), status) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(isManageable){
@@ -90,7 +90,7 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@assignedUserNames.map { userName =>
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){

View File

@@ -73,27 +73,27 @@
</a>
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
@if(body.toString.contains("main-sidebar")) {
@if(body.toString.contains("main-sidebar")){
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button" title="Toggle navigation">
<span class="sr-only">Toggle navigation</span>
</a>
}
@if(!(context.loginAccount.isEmpty && !context.settings.basicBehavior.allowAnonymousAccess)) {
<form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/>
</div>
</form>
<ul class="pc nav navbar-nav">
<form id="search" action="@context.path/search" method="GET" class="pc navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Find a repository" aria-label="Search"/>
</div>
</form>
<ul class="pc nav navbar-nav">
@if(context.loginAccount.isDefined){
<li><a href="@context.path/dashboard/pulls">Pull requests</a></li>
<li><a href="@context.path/dashboard/issues">Issues</a></li>
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
@menu(context).map { link =>
<li><a href="@context.path/@link.path">@link.label</a></li>
}
}
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
@menu(context).map { link =>
<li><a href="@context.path/@link.path">@link.label</a></li>
}
</ul>
}
}
</ul>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
@if(context.loginAccount.isDefined){
@@ -104,7 +104,7 @@
<ul class="dropdown-menu pull-right" style="width: auto;">
<li>
<ul class="menu">
@if(context.settings.basicBehavior.repositoryOperation.create || context.loginAccount.get.isAdmin){
@if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){
<li><a href="@context.path/new">New repository</a></li>
}
<li><a href="@context.path/groups/new">New group</a></li>

View File

@@ -75,7 +75,7 @@
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
@repositoryHeaderComponent(repository, context)
}
@if(repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(context.loginAccount.isEmpty){
<a class="btn btn-default btn-sm" href="@context.path/signin?redirect=@helpers.urlEncode(s"${context.path}/${repository.owner}/${repository.name}")">
<span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span>

View File

@@ -16,8 +16,7 @@
milestones: List[gitbucket.core.model.Milestone],
priorities: List[gitbucket.core.model.Priority],
defaultPriority: Option[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label],
customFields: List[gitbucket.core.model.CustomField])(implicit context: gitbucket.core.controller.Context)
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){
@@ -102,20 +101,7 @@
</div>
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map((_, 0, 0)),
priorities = priorities,
defaultPriority = defaultPriority,
labels = labels,
customFields = customFields.map((_, None)),
isManageable = hasOriginWritePermission,
repository = repository
)
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, defaultPriority, labels, hasOriginWritePermission, repository)
</div>
</div>
</form>

View File

@@ -4,12 +4,10 @@
comments: Seq[gitbucket.core.model.Comment],
changedFileSize: Int,
issueLabels: List[gitbucket.core.model.Label],
issueAsignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
labels: List[gitbucket.core.model.Label],
customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])],
isEditable: Boolean,
isManageable: Boolean,
isManageableForkedRepository: Boolean,
@@ -52,20 +50,7 @@
}
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(
Some(issue),
comments.toList,
issueLabels,
issueAsignees,
collaborators,
milestones,
priorities,
None,
labels,
customFields,
isManageable,
repository
)
@gitbucket.core.issues.html.issueinfo(Some(issue), comments.toList, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
</div>
<script>
$(function(){

View File

@@ -16,7 +16,7 @@
<td>
<div class="col-md-2 text-right">
<a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
<a href="@helpers.url(repository)/commit/@tag.commitId" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.commitId.substring(0, 7)</a><br>
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br>
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
</div>
<div class="col-md-10" style="border-left: 1px solid #eee">

View File

@@ -10,7 +10,7 @@
@defining(repository.tags.find(_.name == release.tag)){ tag =>
@tag.map { tag =>
<a href="@helpers.url(repository)/tree/@helpers.urlEncode(tag.name)" class="strong"><i class="octicon octicon-tag"></i>@tag.name</a><br>
<a href="@helpers.url(repository)/commit/@tag.commitId" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.commitId.substring(0, 7)</a><br>
<a href="@helpers.url(repository)/commit/@tag.id" class="monospace muted"><i class="octicon octicon-git-commit"></i>@tag.id.substring(0, 7)</a><br>
<span class="muted">@gitbucket.core.helper.html.datetimeago(tag.time)</span>
}
}

View File

@@ -244,7 +244,7 @@ function updateHighlighting() {
const isDark = @{highlighterTheme.contains("dark").toString};
if (hash.match(/#L\d+(-L\d+)?/)) {
if (isDark) {
$('li.highlight-dark').removeClass('highlight-dark');
$('li.highlight').removeClass('highlight-dark');
} else {
$('li.highlight').removeClass('highlight');
}

View File

@@ -55,10 +55,10 @@
});
@if(newLineNumber.isDefined){
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number="@newLineNumber"])'));
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.newline[line-number=@newLineNumber])'));
param['diff'] = JSON.stringify(diff);
} else if(oldLineNumber.isDefined){
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number="@oldLineNumber"])'));
var diff = getDiffData($('table[filename="@fileName"] table.diff tr:has(th.line-num.oldline[line-number=@oldLineNumber])'));
param['diff'] = JSON.stringify(diff);
}

View File

@@ -16,7 +16,7 @@
</div>
</fieldset>
</form>
@if(context.settings.basicBehavior.repositoryOperation.rename || context.loginAccount.get.isAdmin){
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){
<form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Rename repository</label>
@@ -33,7 +33,7 @@
</fieldset>
</form>
}
@if(context.settings.basicBehavior.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Transfer Ownership</label>
@@ -50,7 +50,7 @@
</fieldset>
</form>
}
@if(context.settings.basicBehavior.repositoryOperation.delete || context.loginAccount.get.isAdmin){
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group">
<label class="strong">Delete repository</label>

View File

@@ -1,55 +0,0 @@
@(customFields: List[gitbucket.core.model.CustomField],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("Issues", Some(repository)) {
@gitbucket.core.html.menu("settings", repository) {
@gitbucket.core.settings.html.menu("issues", repository) {
<div class="pull-right" style="margin-bottom: 10px;">
<a class="btn btn-success" href="javascript:void(0);" id="new-field-button">New field</a>
</div>
<table class="table table-bordered table-hover table-issues" id="new-field-table" style="display: none;">
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover">
<tr id="field-row-header">
<th>@customFields.size custom fields</th>
</tr>
@customFields.map { customField =>
@gitbucket.core.settings.html.issuesfield(customField)
}
</table>
}
}
}
<script>
$(function(){
$('#new-field-button').click(function(e){
if($('#edit-field-area-new').length != 0){
$('div#edit-field-area-new').remove();
$('#new-field-table').hide();
} else {
$.get('@helpers.url(repository)/settings/issues/fields/new',
function(data){
$('#new-field-table').show().find('tr td').append(data);
}
);
}
});
});
function deleteField(fieldId){
if(confirm('Once you delete this field, there is no going back.\nAre you sure?')){
$.post('@helpers.url(repository)/settings/issues/fields/' + fieldId + '/delete', function(){
$('tr#field-row-' + fieldId).remove();
});
}
}
function editField(fieldId){
$.get('@helpers.url(repository)/settings/issues/fields/' + fieldId + '/edit',
function(data){
$('#field-' + fieldId).hide().parent().append(data);
}
);
}
</script>

View File

@@ -1,28 +0,0 @@
@(customField: gitbucket.core.model.CustomField)
<tr id="field-row-@customField.fieldId">
<td>
<div class="row" id="field-@customField.fieldId">
<div class="col-md-4 strong">
@customField.fieldName
</div>
<div class="col-md-4">
@customField.fieldType
</div>
<div class="col-md-2">
@if(customField.enableForIssues) {
Issues &nbsp;&nbsp;
}
@if(customField.enableForPullRequests) {
Pull requests
}
</div>
<div class="col-md-2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editField(@customField.fieldId)">Edit</a>
&nbsp;&nbsp;
<a href="javascript:void(0);" onclick="deleteField(@customField.fieldId)">Delete</a>
</div>
</div>
</div>
</td>
</tr>

View File

@@ -1,66 +0,0 @@
@(field: Option[gitbucket.core.model.CustomField],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@defining(field.map(_.fieldId).getOrElse("new")){ fieldId =>
<div id="edit-field-area-@fieldId">
<form class="form-inline" autocomplete="off">
<input type="text" id="fieldName-@fieldId" style="width: 300px; float: left; margin-right: 4px;" class="form-control input-sm" value="@field.map(_.fieldName)"@if(fieldId == "new"){ placeholder="New field name"}/>
<select id="fieldType-@fieldId" class="form-control input-sm">
<option value="long" @if(field.map(_.fieldType == "long").getOrElse(false)){selected}>Long</option>
<option value="double" @if(field.map(_.fieldType == "double").getOrElse(false)){selected}>Double</option>
<option value="string" @if(field.map(_.fieldType == "string").getOrElse(false)){selected}>String</option>
<option value="date" @if(field.map(_.fieldType == "date").getOrElse(false)){selected}>Date</option>
</select>
<label for="enableForIssues-@fieldId" class="normal" style="margin-left: 4px;">
<input type="checkbox" id="enableForIssues-@fieldId" @if(field.map(_.enableForIssues).getOrElse(false)){checked}> Issues
</label>
<label for="enableForPullRequests-@fieldId" class="normal" style="margin-left: 4px;">
<input type="checkbox" id="enableForPullRequests-@fieldId" @if(field.map(_.enableForPullRequests).getOrElse(false)){checked}> Pull requests
</label>
<span class="pull-right">
<span id="field-error-@fieldId" class="error"></span>
<input type="button" id="cancel-@fieldId" class="btn btn-sm btn-default field-edit-cancel" value="Cancel">
<input type="button" id="submit-@fieldId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(fieldId == "new") "Create field" else "Save changes")"/>
</span>
</form>
</div>
<script>
$(function(){
$('#submit-@fieldId').click(function(e){
$.post('@helpers.url(repository)/settings/issues/fields/@{if(fieldId == "new") "new" else s"$fieldId/edit"}', {
'fieldName' : $('#fieldName-@fieldId').val(),
'fieldType': $('#fieldType-@fieldId option:selected').val(),
'enableForIssues': $('#enableForIssues-@fieldId').prop('checked'),
'enableForPullRequests': $('#enableForPullRequests-@fieldId').prop('checked')
}, function(data, status){
$('div#edit-field-area-@fieldId').remove();
@if(fieldId == "new"){
$('#new-field-table').hide();
// Insert row into the top of table
$('#field-row-header').after(data);
} else {
// Replace table row
$('#field-row-@fieldId').after(data).remove();
}
}).fail(function(xhr, status, error){
const errors = JSON.parse(xhr.responseText);
if(errors.fieldName){
$('span#field-error-@fieldId').text(errors.fieldName);
} else {
$('span#field-error-@fieldId').text('error');
}
});
return false;
});
$('#cancel-@fieldId').click(function(e){
$('div#edit-field-area-@fieldId').remove();
@if(fieldId == "new"){
$('#new-field-table').hide();
} else {
$('#field-@fieldId').show();
}
});
});
</script>
}

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