Compare commits

...

134 Commits
4.3 ... 4.7.1

Author SHA1 Message Date
Naoki Takezoe
7c575fdc52 Update README.md 2016-11-28 00:17:12 +09:00
Naoki Takezoe
f9d6f1334f Update README.md 2016-11-28 00:16:06 +09:00
Naoki Takezoe
65d4900325 Update version to 4.7.1 2016-11-28 00:08:44 +09:00
Naoki Takezoe
bec75120bc Update version to 4.7.1 2016-11-27 19:09:45 +09:00
Naoki Takezoe
3b0eed48d9 (refs #1355)Fix getUserRepositories() and getAllRepositories() too 2016-11-27 19:03:58 +09:00
Naoki Takezoe
25c4b1e6a7 (refs #1355)Fix RepositoryService#getVisibleRepositories() condition 2016-11-27 16:42:28 +09:00
Naoki Takezoe
cccff46715 (refs #1355)Fix RepositoryService#getVisibleRepositories() to contain group repositories 2016-11-27 04:03:23 +09:00
Matthieu Brouillard
fc0ffd1b4f Merge pull request #1356 from uli-heller/typo_ans_guests
Fixed typo in 4.7: '... ans guests...' -> '... and guests...'
2016-11-26 12:25:06 +01:00
Uli Heller
b70a2a2327 Fixed typo 2016-11-26 11:24:28 +01:00
Naoki Takezoe
7bb5379b45 Update for 4.7 release 2016-11-26 13:31:12 +09:00
Naoki Takezoe
5692a8c83e Fix update branch authentication 2016-11-26 13:07:54 +09:00
Naoki Takezoe
6c6126148e Refactor new permission system 2016-11-26 13:00:24 +09:00
Naoki Takezoe
5b2e24daef Merge pull request #1353 from mashabow/link-milestone-label
Link milestone label to its issue list
2016-11-25 12:01:13 +09:00
Masaya Nakamura
29f390e48c Use helpers.urlEncode()/encodeURIComponent() 2016-11-25 11:40:05 +09:00
Masaya Nakamura
c49eff6e54 Link milestone label to its issue list 2016-11-24 17:35:49 +09:00
Naoki Takezoe
27930a5849 Fix testcase 2016-11-20 00:00:58 +09:00
Naoki Takezoe
6ffc139d2f (refs #1275)Keep sidebar status during same session 2016-11-19 23:02:41 +09:00
Naoki Takezoe
59ed027b60 Distinct assignable user names 2016-11-19 21:28:39 +09:00
Naoki Takezoe
5dc55822d7 Fix multi statement splitting 2016-11-17 00:16:02 +09:00
Naoki Takezoe
9bfe5115cc (refs #1350) Accept max 100 characters as repository name 2016-11-15 02:21:55 +09:00
Naoki Takezoe
aaf9c65f30 Fix scaladoc 2016-11-12 18:29:03 +09:00
Naoki Takezoe
d6197261fb (refs #1343) Keep file permission in online file editing 2016-11-09 10:34:53 +09:00
Naoki Takezoe
8fd7df2a9d (refs #1345) Remove large size avatar image to use width efficiently 2016-11-09 02:11:21 +09:00
Naoki Takezoe
4eb148f4a6 (refs #1340)Add dropdown filter
Because the large dropdown list makes  impossible to choose items below the screen.
2016-11-08 21:19:37 +09:00
Naoki Takezoe
8f1e460893 Merge pull request #1338 from gitbucket/new-permission-system
New permission system
2016-11-08 18:04:17 +09:00
Naoki Takezoe
8c80f8a506 (refs #1286) Rename CollaboratorsAuthenticator to WritableUsersAutheticator 2016-11-08 17:48:28 +09:00
Naoki Takezoe
9eb9fc666c (refs #1286) Bugfix 2016-11-08 02:32:11 +09:00
Naoki Takezoe
d70c6cece7 (refs #1286) Fix testcase 2016-11-06 16:37:44 +09:00
Naoki Takezoe
dbdee135a3 (refs #1286) Update collaborators setting form 2016-11-06 16:32:46 +09:00
Naoki Takezoe
132bb6bee4 (refs #1286) Update controllers 2016-11-04 13:57:39 +09:00
Naoki Takezoe
2dfa7a1190 (refs #1286) Update the repository settings form 2016-11-02 10:34:28 +09:00
Naoki Takezoe
06d559b47e (refs #1286) Add columns: ISSUES_OPTION and WIKI_OPTION 2016-11-02 02:15:21 +09:00
Naoki Takezoe
83baaa6ed9 (refs #1286) Show group marker when collaborator is added 2016-11-01 21:04:27 +09:00
Naoki Takezoe
85d38a47f1 (refs #1286) Refactoring 2016-11-01 18:01:57 +09:00
Naoki Takezoe
0c3c6ea15a (refs #1286) Show whether group account on the collaborators proposal 2016-11-01 16:03:02 +09:00
Naoki Takezoe
2ce436bddc Merge pull request #1336 from yanma/issue_1318
(refs #1318) make record***Activity via ssh works again
2016-11-01 14:16:26 +09:00
Hiroaki Yamazoe
a60c607fcb enable transaction for SSH access 2016-11-01 11:56:36 +09:00
Naoki Takezoe
0456739118 (refs #1286) Update collaborators setting form 2016-11-01 09:10:40 +09:00
Naoki Takezoe
368052bd8f (refs #1286) Fix services and beat compilation errors 2016-11-01 07:24:51 +09:00
Naoki Takezoe
ce916a7d4b (refs #1286) Fix models 2016-11-01 06:51:30 +09:00
Naoki Takezoe
60ff046823 (refs #1286) Prototyping of new permission system 2016-11-01 06:45:18 +09:00
Naoki Takezoe
7d3bda42e2 Update version 2016-10-29 15:00:20 +09:00
Naoki Takezoe
83a39f1e39 Update README.md 2016-10-29 15:00:01 +09:00
Naoki Takezoe
de726d8d96 (refs #1325) Prepend one more empty line when the first line is an empty line. 2016-10-26 12:21:21 +09:00
Naoki Takezoe
91bb241e8c (refs #1334) Indicate who is group manager 2016-10-26 10:44:20 +09:00
Naoki Takezoe
8da55d8aa8 Merge pull request #1311 from gitbucket/fix-issues-sorting
(refs #1308)Fix issues sorting
2016-10-19 01:29:00 +09:00
Naoki Takezoe
3355c46503 (refs #1308)Fix issues sorting again 2016-10-18 02:30:23 +09:00
Naoki Takezoe
0a3d457218 Merge pull request #1328 from kw-udon/custom-media-type-in-content-api
Support custom media types in get-content API
2016-10-17 09:31:24 +09:00
Naoki Takezoe
7fa5fdfbd0 Merge pull request #1326 from kounoike/pr/suppress-transition-on-load-in-ie
Suppress noisy transition animation on load in IE11
2016-10-17 01:14:43 +09:00
Naoki Takezoe
95f88891d0 Merge pull request #1327 from kounoike/pr/fix-logo
Cleanup white pixels in gitbucket logo.
2016-10-17 01:13:20 +09:00
Keiichi Watanabe
550f8f415c Support custom media types in get-content API
cf. https://developer.github.com/v3/repos/contents/#custom-media-types
2016-10-16 20:31:26 +09:00
KOUNOIKE Yuusuke
5ab947d8ec Cleanup white pixels in gitbucket logo. 2016-10-16 13:02:20 +09:00
KOUNOIKE Yuusuke
ec793535e7 Suppress noisy transition animation on load in IE11
http://stackoverflow.com/a/25674229
2016-10-16 12:44:43 +09:00
Naoki Takezoe
2f1d81cc4c Create issue comment by online file editing as well 2016-10-13 20:36:11 +09:00
Naoki Takezoe
0f189ca710 (refs #1319)Get rid of the duplication of issue id extracted from commit message 2016-10-13 20:24:01 +09:00
Naoki Takezoe
6afd51bb8d (refs #1312)Fix badge position on the side menu 2016-10-13 06:42:58 +09:00
Naoki Takezoe
e415f9d24e (refs #1316)Add "Page History" button to the wiki page view 2016-10-13 00:59:19 +09:00
Naoki Takezoe
ba5d587a1e Merge pull request #1321 from int128/gh-compatibility
Improve GitHub compatibility for Jenkins
2016-10-11 12:32:43 +09:00
Naoki Takezoe
92f778b6e9 Merge pull request #1320 from kw-udon/file-content-api
Add API to get a file content
2016-10-10 18:12:03 +09:00
Hidetake Iwata
b52981a845 Provide GitHub compatible URL for Git clients 2016-10-10 15:13:47 +09:00
Keiichi Watanabe
9c5d3edc72 Add API to get a file content 2016-10-10 02:04:43 +09:00
Naoki Takezoe
56d68c6145 Merge pull request #1313 from xuwei-k/remove-scalaz
remove unused scalaz
2016-10-05 17:45:01 +09:00
xuwei-k
4d13282915 remove unused scalaz 2016-10-05 12:14:43 +09:00
Naoki Takezoe
872320ccab (refs #1129)Not use Option.get for non-able value 2016-10-04 10:28:47 +09:00
Naoki Takezoe
28ee80b727 (refs #1129)Not delete from REPOSITORY table when user is disabled 2016-10-03 16:55:20 +09:00
Naoki Takezoe
2621de2cde Fix error responses 2016-10-03 16:01:57 +09:00
Naoki Takezoe
82b102845f (refs #1292)Add new option to disable repository forking 2016-10-03 15:26:23 +09:00
Naoki Takezoe
28c9f8b89a (refs #1308)Fix sorting in issue query 2016-10-03 01:42:56 +09:00
Naoki Takezoe
23fa937fd1 Remove unnecessary lines 2016-10-02 02:27:48 +09:00
Naoki Takezoe
02330a2050 (refs #1304)Remove package artifact overriding 2016-10-01 03:24:21 +09:00
Naoki Takezoe
c65599d995 Update README.md 2016-09-29 10:40:21 +09:00
Naoki Takezoe
22ae1df4b1 Update README.md 2016-09-29 10:30:34 +09:00
Naoki Takezoe
6b22342166 Merge pull request #1299 from gitbucket/release/gitbucket-4.5
GitBucket 4.5 release
2016-09-29 10:29:07 +09:00
Naoki Takezoe
53f6190267 Scalaz's <| is deprecated 2016-09-28 14:05:40 +09:00
Naoki Takezoe
f73daaef44 (refs #954)Cut commit id in Markdown with 7 letters 2016-09-28 13:28:50 +09:00
Naoki Takezoe
d99e382dfe (refs #1206)Display commit count on the history button 2016-09-28 10:11:55 +09:00
Naoki Takezoe
aefbee2093 (refs #1206)Display find and history icon in mobile view 2016-09-28 09:58:32 +09:00
Naoki Takezoe
11fb0a7edf (refs #1214)Gravater is disable in default 2016-09-28 09:33:16 +09:00
Naoki Takezoe
fe959aecff (refs #1298)Append raw=true only if the given url does not have it. 2016-09-25 17:42:50 +09:00
Naoki Takezoe
9b33655bd4 Bump to sbt 0.13.12. 2016-09-25 17:09:29 +09:00
Naoki Takezoe
33acad85db Merge pull request #1301 from conradlink/master
Fix host command line argument
2016-09-23 16:00:28 +09:00
conradlink
6bfe3ea760 Merge remote-tracking branch 'upstream/master' 2016-09-23 00:32:35 -04:00
Naoki Takezoe
1532fd71d0 Remove files for publishing jars to the maven repository because already it's possible by sbt. 2016-09-22 11:43:21 +09:00
Naoki Takezoe
c14a732e2a Update publishing jar operation 2016-09-22 11:41:26 +09:00
Naoki Takezoe
a1372034ed Ready for GitBucket 4.5 release 2016-09-22 11:37:37 +09:00
conradlink
98914269b7 Fix to make the --host argument work again. 2016-09-21 08:43:07 -04:00
Naoki Takezoe
d5e455336b (refs #1293)Restore dashboard issues / pull requests switcher 2016-09-20 16:41:20 +09:00
Naoki Takezoe
7b84f25c56 (refs #1297)Bugfix 2016-09-20 15:45:21 +09:00
Naoki Takezoe
2ca20af502 (refs #1297)Allow to configure HikariCP in database.conf 2016-09-19 23:13:52 +09:00
Naoki Takezoe
78df2accfc (refs #1290)Always show “Download ZIP” button 2016-09-18 21:14:00 +09:00
Naoki Takezoe
7a282fb67e (refs #1291)Add secure attribute to JSESSIONID cookie when baseUrl starts with "https://" 2016-09-12 15:06:59 +09:00
Naoki Takezoe
db679967af (refs #1291)Add http-only attribute to JSESSIONID cookie 2016-09-12 14:59:43 +09:00
Naoki Takezoe
9e98d30612 (refs #1288)Dropping files is also available in textarea, not only in the bottom bar. 2016-09-06 02:05:23 +09:00
Naoki Takezoe
a47065e4a9 (refs #1277)Don't make search filter and sorting sticky 2016-09-05 11:55:44 +09:00
Naoki Takezoe
94d18c471c Remove unnecessary style class 2016-09-04 02:59:06 +09:00
Naoki Takezoe
f8f3019228 Tweak branch list presentation 2016-09-04 02:54:54 +09:00
Naoki Takezoe
c3d90b8593 (refs #1239)Add toggle sidebar button 2016-09-04 02:12:56 +09:00
Naoki Takezoe
62c1299f29 (refs #1275, #1239)Fix CSS 2016-09-04 00:51:08 +09:00
Naoki Takezoe
b75db98cad (refs #1278) Bump to AdminLTE 2.3.6 2016-08-31 01:16:52 +09:00
Naoki Takezoe
3592b3d13c Merge pull request #1280 from kw-udon/fix-path-param-contents-api
fix get-contents API's format
2016-08-31 01:11:18 +09:00
Keiichi Watanabe
ca814e2c08 fix path parameter in get-contents API 2016-08-30 19:00:10 +09:00
Naoki Takezoe
48b6a590bf 4.4.0 release 2016-08-28 11:51:59 +09:00
Naoki Takezoe
285ef02a17 Merge pull request #1270 from S-YOU/patch-1
add copyright holder name in license
2016-08-28 11:10:42 +09:00
YOU
18375c741e Update LICENSE 2016-08-26 08:05:40 +00:00
Naoki Takezoe
21030344cc Merge pull request #1273 from kw-udon/fix-contents-api-default-ref-param
Set default value of parameter in Get-Contents API
2016-08-22 23:55:18 +09:00
Keiichi Watanabe
a494027217 Set default value to ref param in contents-API 2016-08-22 18:58:59 +09:00
Naoki Takezoe
7bca01af59 (refs #1230) Apply table-hover class for the commit history 2016-08-18 11:48:47 +09:00
YOU
acf3fa9980 add copyright holder name in license 2016-08-15 23:32:31 +09:00
Naoki Takezoe
c0ce0f8d19 (refs #1259) Add SQL import capability and remove XML export / import 2016-08-14 01:55:19 +09:00
Naoki Takezoe
56e7168461 Merge pull request #1268 from haru/FixgetGroupNames
Fix AccountService#getGroupNames returns duplicated group name .
2016-08-12 00:08:48 +09:00
Haruyuki Iida
c2d0d94f05 Fix AccountService#getGroupNames returns duplicated group name . 2016-08-11 15:28:29 +09:00
Naoki Takezoe
fc22cfbbdd Merge branch 'master' of https://github.com/gitbucket/gitbucket 2016-08-11 14:40:14 +09:00
Naoki Takezoe
d62adbf649 (refs #769) Output go-import meta tag in all repositories for go get 2016-08-11 14:40:06 +09:00
Naoki Takezoe
dba5539e3e (refs #1267) Graceful shutdown for the embedded jetty 2016-08-10 20:57:44 +09:00
Naoki Takezoe
f0a8b3bb17 (refs #1264) BugFix: File attachment does not work on the issue comment 2016-08-08 21:57:26 +09:00
Naoki Takezoe
f52e7e1bdd (refs #1255) Display the newest version as plugin version because if migration was failed, plugin is not registered. 2016-08-07 14:10:57 +09:00
Naoki Takezoe
58ba26f21e (refs #1255) Also check plugin version 2016-08-07 13:58:51 +09:00
Naoki Takezoe
bf7b30630c (refs #1255) Check whether database version is same as GitBucket version in startup 2016-08-07 13:42:42 +09:00
Naoki Takezoe
b5cac0308e Merge branch 'mcveat-217-sort-milestones' 2016-08-06 10:04:18 +09:00
Naoki Takezoe
373ea39048 (refs #219) Add milestoneId as a lowest priority sort column 2016-08-06 10:03:59 +09:00
Naoki Takezoe
427f5eec5f Merge branch '217-sort-milestones' of https://github.com/mcveat/gitbucket into mcveat-217-sort-milestones
# Conflicts:
#	src/main/scala/service/MilestonesService.scala
2016-08-06 09:56:56 +09:00
Naoki Takezoe
a4e9903e00 Merge pull request #1261 from UprootStaging/sshdUpdate
Sshd update
2016-08-04 15:51:03 +09:00
Naoki Takezoe
0d900a892c Merge pull request #1260 from team-lab/fix-blame-admin-lte
(fix #1246) cannot see the blame
2016-08-04 15:28:23 +09:00
hrj
dc6fdaf482 Fix for test compilation 2016-08-04 10:03:35 +05:30
hrj
b79498ed9f Update apache-sshd to latest version 2016-08-04 09:54:07 +05:30
nazoking
69e8f628df (fix #1246) cannot see the blame 2016-08-04 12:46:02 +09:00
Naoki Takezoe
d3d8e3ce5f Merge pull request #1256 from team-lab/git-is-reserved-user-name
(refs #1251) git is reserved user name. add validation.
2016-08-03 15:42:13 +09:00
nazoking
0499c47f4b (refs #1251, #1256) add admin, upload and api to reserved. 2016-08-03 12:14:32 +09:00
nazoking
7fd0cdd7d8 (refs #1251) git is reserved user name 2016-08-02 18:00:40 +09:00
Naoki Takezoe
49eaf79e01 Merge pull request #1253 from gitbucket/publish-maven-central
(refs #935) Update project configuration for deploying artifacts to Maven central
2016-07-31 21:45:04 +09:00
Naoki Takezoe
3a96c30aa8 (refs #935) Reoveride artifact to remove war from published artifacts 2016-07-31 21:21:27 +09:00
Naoki Takezoe
6d550fa485 (refs #935) Revert version to 4.3.0 2016-07-31 20:49:16 +09:00
Naoki Takezoe
7f9d69bb51 (refs #935) Update project configuration for deploying artifacts to Maven central 2016-07-31 18:08:47 +09:00
Piotr Adamski
3cc7bd3cdb #217 open milestones sorted by due date ascending, closed milestones sorted by close date descending 2013-12-01 14:09:25 +01:00
181 changed files with 4832 additions and 1748 deletions

View File

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2013-2016 GitBucket Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -65,6 +65,37 @@ Support
Release Notes
-------------
### 4.7.1 - 28 Nov 2016
- Bug fix: group repositories are not shown in the your repositories list on the sidebar
- Small performance improvement of the dashboard
### 4.7 - 26 Nov 2016
- New permission system
- Dropdown filter for issue labels, milestones and assignees
- Keep sidebar folding status
- Link from milestone label to the issue list
### 4.6 - 29 Oct 2016
- Add disable option for forking
- Add History button to wiki page
- Git repository URL redirection for GitHub compatibility
- Get-Content API improvement
- Indicate who is group master in Members tab in group view
### 4.5 - 29 Sep 2016
- Attach files by dropping into textarea
- Issues / Pull requests switcher in dashboard
- HikariCP could be configured in `GITBUCKET_HOME/database.conf`
- Improve Cookie security
- Display commit count on the history button
- Improve mobile view
### 4.4 - 28 Aug 2016
- Import a SQL dump file to the database
- `go get` support in private repositories
- Sort milestones by due date
- apache-sshd has been updated to 1.2.0
### 4.3 - 30 Jul 2016
- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- User name suggestion

View File

@@ -1,6 +1,6 @@
val Organization = "gitbucket"
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.3.0"
val GitBucketVersion = "4.7.1"
val ScalatraVersion = "2.4.1"
val JettyVersion = "9.3.9.v20160517"
@@ -33,7 +33,7 @@ libraryDependencies ++= Seq(
"org.apache.commons" % "commons-compress" % "1.11",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
"org.apache.sshd" % "apache-sshd" % "1.0.0",
"org.apache.sshd" % "apache-sshd" % "1.2.0",
"org.apache.tika" % "tika-core" % "1.13",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
@@ -50,7 +50,6 @@ libraryDependencies ++= Seq(
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.scalaz" %% "scalaz-core" % "7.2.4" % "test",
"com.wix" % "wix-embedded-mysql" % "1.0.3" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
)
@@ -106,7 +105,6 @@ libraryDependencies ++= Seq(
val executableKey = TaskKey[File]("executable")
executableKey := {
import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName }
@@ -164,9 +162,55 @@ executableKey := {
log info s"built executable webapp ${outputFile}"
outputFile
}
/*
Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war"))
publishTo <<= version { (v: String) =>
val nexus = "https://oss.sonatype.org/"
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/
publishMavenStyle := true
pomIncludeRepository := { _ => false }
pomExtra := (
<url>https://github.com/gitbucket/gitbucket</url>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<scm>
<url>https://github.com/gitbucket/gitbucket</url>
<connection>scm:git:https://github.com/gitbucket/gitbucket.git</connection>
</scm>
<developers>
<developer>
<id>takezoe</id>
<name>Naoki Takezoe</name>
<url>https://github.com/takezoe</url>
</developer>
<developer>
<id>shimamoto</id>
<name>Takako Shimamoto</name>
<url>https://github.com/shimamoto</url>
</developer>
<developer>
<id>tanacasino</id>
<name>Tomofumi Tanaka</name>
<url>https://github.com/tanacasino</url>
</developer>
<developer>
<id>mrkm4ntr</id>
<name>Shintaro Murakami</name>
<url>https://github.com/mrkm4ntr</url>
</developer>
<developer>
<id>nazoking</id>
<name>nazoking</name>
<url>https://github.com/nazoking</url>
</developer>
<developer>
<id>McFoggy</id>
<name>Matthieu Brouillard</name>
<url>https://github.com/McFoggy</url>
</developer>
</developers>
)

View File

@@ -46,9 +46,10 @@ $ sbt executable
### Deploy assembly jar file
For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`.
For plug-in development, we have to publish the GitBucket jar file to the Maven central repository as well. At first, hit following command to publish artifacts to the sonatype OSS repository:
```bash
$ cd release/
$ ./deploy-assembly-jar.sh
$ sbt publish-signed
```
Then operate release sequence at https://oss.sonatype.org/.

View File

@@ -1 +1 @@
sbt.version=0.13.11
sbt.version=0.13.12

View File

@@ -1,6 +1,7 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")

View File

@@ -1,24 +0,0 @@
#!/bin/sh
. ./env.sh
cd ../
./sbt.sh clean assembly
cd release
if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then
MVN_DEPLOY_PATH=mvn-snapshot
else
MVN_DEPLOY_PATH=mvn
fi
echo $MVN_DEPLOY_PATH
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=$GITBUCKET_VERSION\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/

View File

@@ -1,3 +0,0 @@
#!/bin/sh
export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"

View File

@@ -1,17 +0,0 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jp.sf.amateras</groupId>
<artifactId>gitbucket-assembly</artifactId>
<version>0.0.1</version>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.10</version>
</extension>
</extensions>
</build>
</project>

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.12.jar" %*

2
sbt.sh
View File

@@ -1,2 +1,2 @@
#!/bin/sh
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.12.jar "$@"

View File

@@ -3,12 +3,14 @@ import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
import java.net.URL;
import java.net.InetSocketAddress;
import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
String host = null;
int port = 8080;
InetSocketAddress address = null;
String contextPath = "/";
boolean forceHttps = false;
@@ -29,7 +31,13 @@ public class JettyLauncher {
}
}
Server server = new Server(port);
if(host != null) {
address = new InetSocketAddress(host, port);
} else {
address = new InetSocketAddress(port);
}
Server server = new Server(address);
// SelectChannelConnector connector = new SelectChannelConnector();
// if(host != null) {
@@ -60,6 +68,8 @@ public class JettyLauncher {
}
server.setHandler(context);
server.setStopAtShutdown(true);
server.setStopTimeout(7_000);
server.start();
server.join();
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="REPOSITORY">
<column name="ALLOW_FORK" type="boolean" nullable="false" defaultValueBoolean="true"/>
</addColumn>
</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

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="COLLABORATOR">
<column name="ROLE" type="varchar(10)" nullable="false" defaultValue="ADMIN"/>
</addColumn>
<addColumn tableName="REPOSITORY">
<column name="WIKI_OPTION" type="varchar(10)" nullable="false" defaultValue="DISABLE"/>
<column name="ISSUES_OPTION" type="varchar(10)" nullable="false" defaultValue="DISABLE"/>
</addColumn>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="DISABLE"/>
<where>ENABLE_WIKI = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="PRIVATE"/>
<where>ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="PUBLIC"/>
<where>ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = TRUE</where>
</update>
<update tableName="REPOSITORY">
<column name="ISSUES_OPTION" value="DISABLE"/>
<where>ENABLE_ISSUES = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="ISSUES_OPTION" value="PUBLIC"/>
<where>ENABLE_ISSUES = TRUE</where>
</update>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/>
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
</changeSet>

View File

@@ -1,17 +1,23 @@
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{ApiAuthenticationFilter, GitAuthenticationFilter, Database, TransactionFilter}
import gitbucket.core.util.Directory
import java.util.EnumSet
import javax.servlet._
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.servlet._
import gitbucket.core.util.Directory
import org.scalatra._
class ScalatraBootstrap extends LifeCycle {
class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
override def init(context: ServletContext) {
val settings = loadSystemSettings()
if(settings.baseUrl.exists(_.startsWith("https://"))) {
context.getSessionCookieConfig.setSecure(true)
}
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
@@ -19,6 +25,9 @@ class ScalatraBootstrap extends LifeCycle {
context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
context.addFilter("ghCompatRepositoryAccessFilter", new GHCompatRepositoryAccessFilter)
context.getFilterRegistration("ghCompatRepositoryAccessFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")

View File

@@ -13,5 +13,15 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new LiquibaseMigration("update/gitbucket-core_4.2.xml")
),
new Version("4.2.1"),
new Version("4.3.0")
new Version("4.3.0"),
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 SqlMigration("update/gitbucket-core_4.7.sql")
),
new Version("4.7.1")
)

View File

@@ -1,11 +1,18 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil.FileInfo
import org.apache.commons.codec.binary.Base64
case class ApiContents(`type`: String, name: String)
case class ApiContents(`type`: String, name: String, content: Option[String], encoding: Option[String])
object ApiContents{
def apply(fileInfo: FileInfo): ApiContents =
if(fileInfo.isDirectory) ApiContents("dir", fileInfo.name)
else ApiContents("file", fileInfo.name)
}
def apply(fileInfo: FileInfo, content: Option[Array[Byte]]): ApiContents = {
if(fileInfo.isDirectory) {
ApiContents("dir", fileInfo.name, None, None)
} else {
content.map(arr =>
ApiContents("file", fileInfo.name, Some(Base64.encodeBase64String(arr)), Some("base64"))
).getOrElse(ApiContents("file", fileInfo.name, None, None))
}
}
}

View File

@@ -30,7 +30,7 @@ object ApiUser{
def apply(user: Account): ApiUser = ApiUser(
login = user.userName,
email = user.mailAddress,
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
`type` = if(user.isGroupAccount){ "Organization" } else { "User" },
site_admin = user.isAdmin,
created_at = user.registeredDate
)

View File

@@ -11,7 +11,7 @@ case class CreateARepository(
auto_init: Boolean = false
) {
def isValid: Boolean = {
name.length<=40 &&
name.length <= 100 &&
name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
!name.startsWith("_") &&
!name.startsWith("-")

View File

@@ -14,6 +14,7 @@ import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.scalatra.BadRequest
class AccountController extends AccountControllerBase
@@ -38,7 +39,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class PersonalTokenForm(note: String)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
@@ -68,7 +69,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
@@ -120,7 +121,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Members
case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName)
gitbucket.core.account.html.members(account, members.map(_.userName),
gitbucket.core.account.html.members(account, members,
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
@@ -133,7 +134,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
}
} getOrElse NotFound
} getOrElse NotFound()
}
get("/:userName.atom") {
@@ -156,7 +157,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
html.edit(x, flash.get("info"), flash.get("error"))
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -172,7 +173,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
flash += "info" -> "Account information has been updated."
redirect(s"/${userName}/_edit")
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:userName/_delete")(oneselfOnly {
@@ -196,14 +197,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
session.invalidate
redirect("/")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
@@ -234,7 +235,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case _ => None
}
html.application(x, tokens, generatedToken)
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
@@ -260,7 +261,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} else {
html.register()
}
} else NotFound
} else NotFound()
}
post("/register", newForm){ form =>
@@ -268,7 +269,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound
} else NotFound()
}
get("/groups/new")(usersOnly {
@@ -318,18 +319,18 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
// // Update COLLABORATOR for group repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// removeCollaborators(form.groupName, repositoryName)
// members.foreach { case (userName, isManager) =>
// addCollaborator(form.groupName, repositoryName, userName)
// }
// }
updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}")
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -355,76 +356,80 @@ trait AccountControllerBase extends AccountManagementControllerBase {
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
if(repository.repository.options.allowFork){
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
} else BadRequest()
})
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
if(repository.repository.options.allowFork){
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
insertRepository(
repositoryName = repository.name,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
insertRepository(
repositoryName = repository.name,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Add collaborators for group repository
val ownerAccount = getAccountByUserName(accountName).get
if(ownerAccount.isGroupAccount){
getGroupMembers(accountName).foreach { member =>
addCollaborator(accountName, repository.name, member.userName)
}
// // Add collaborators for group repository
// val ownerAccount = getAccountByUserName(accountName).get
// if(ownerAccount.isGroupAccount){
// getGroupMembers(accountName).foreach { member =>
// addCollaborator(accountName, repository.name, member.userName)
// }
// }
// Insert default labels
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${accountName}/${repository.name}")
}
// Insert default labels
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${accountName}/${repository.name}")
}
}
} else BadRequest()
})
private def existsAccount: Constraint = new Constraint(){

View File

@@ -7,9 +7,10 @@ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil.{CommitInfo, getFileList, getBranches, getDefaultBranch}
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.view.helpers.{renderMarkup, isRenderable}
import org.eclipse.jgit.api.Git
import org.scalatra.{NoContent, UnprocessableEntity, Created}
import scala.collection.JavaConverters._
@@ -34,7 +35,7 @@ class ApiController extends ApiControllerBase
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator
with WritableUsersAuthenticator
trait ApiControllerBase extends ControllerBase {
self: RepositoryService
@@ -51,7 +52,7 @@ trait ApiControllerBase extends ControllerBase {
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator =>
with WritableUsersAuthenticator =>
/**
* https://developer.github.com/v3/#root-endpoint
@@ -66,7 +67,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/orgs/:groupName") {
getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
} getOrElse NotFound()
}
/**
@@ -75,7 +76,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
} getOrElse NotFound()
}
/**
@@ -109,13 +110,52 @@ trait ApiControllerBase extends ControllerBase {
* https://developer.github.com/v3/repos/contents/#get-contents
*/
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
val (id, path) = repository.splitPath(multiParams("splat").head)
val refStr = params("ref")
def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = {
val path = new java.io.File(pathStr)
val dirName = path.getParent match {
case null => "."
case s => s
}
getFileList(git, revision, dirName).find(f => f.name.equals(path.getName))
}
val path = multiParams("splat").head match {
case s if s.isEmpty => "."
case s => s
}
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git =>
if (path.isEmpty) {
JsonFormat(getFileList(git, refStr, ".").map{f => ApiContents(f)})
} else {
JsonFormat(getFileList(git, refStr, path).map{f => ApiContents(f)})
val fileList = getFileList(git, refStr, path)
if (fileList.isEmpty) { // file or NotFound
getFileInfo(git, refStr, path).flatMap(f => {
val largeFile = params.get("large_file").exists(s => s.equals("true"))
val content = getContentFromId(git, f.id, largeFile)
request.getHeader("Accept") match {
case "application/vnd.github.v3.raw" =>
content
case "application/vnd.github.v3.html" if isRenderable(f.name) =>
content.map(c =>
List(
"<div data-path=\"", path, "\" id=\"file\">", "<article>",
renderMarkup(path.split("/").toList, new String(c), refStr, repository, false, false, true).body,
"</article>", "</div>"
).mkString
)
case "application/vnd.github.v3.html" =>
content.map(c =>
List(
"<div data-path=\"", path, "\" id=\"file\">", "<div class=\"plain\">", "<pre>",
play.twirl.api.HtmlFormat.escape(new String(c)).body,
"</pre>", "</div>", "</div>"
).mkString
)
case _ =>
Some(JsonFormat(ApiContents(f, content)))
}
}).getOrElse(NotFound())
} else { // directory
JsonFormat(fileList.map{f => ApiContents(f, None)})
}
}
})
@@ -137,7 +177,8 @@ trait ApiControllerBase extends ControllerBase {
* https://developer.github.com/v3/repos/collaborators/#list-collaborators
*/
get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository =>
JsonFormat(getCollaborators(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get)))
// TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members.
JsonFormat(getCollaboratorUserNames(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get)))
})
/**
@@ -146,7 +187,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
} getOrElse Unauthorized()
}
/**
@@ -180,7 +221,7 @@ trait ApiControllerBase extends ControllerBase {
)
}
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -204,7 +245,7 @@ trait ApiControllerBase extends ControllerBase {
)
}
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -222,7 +263,7 @@ trait ApiControllerBase extends ControllerBase {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -244,7 +285,7 @@ trait ApiControllerBase extends ControllerBase {
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield {
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}).getOrElse(NotFound)
}) getOrElse NotFound()
})
/**
@@ -260,7 +301,7 @@ trait ApiControllerBase extends ControllerBase {
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -287,7 +328,7 @@ trait ApiControllerBase extends ControllerBase {
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
post("/api/v3/repos/:owner/:repository/labels")(writableUsersOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
@@ -312,7 +353,7 @@ trait ApiControllerBase extends ControllerBase {
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
@@ -338,7 +379,7 @@ trait ApiControllerBase extends ControllerBase {
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
@@ -394,7 +435,7 @@ trait ApiControllerBase extends ControllerBase {
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)))
}).getOrElse(NotFound)
}) getOrElse NotFound()
})
/**
@@ -413,7 +454,7 @@ trait ApiControllerBase extends ControllerBase {
JsonFormat(commits)
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
/**
@@ -426,7 +467,7 @@ trait ApiControllerBase extends ControllerBase {
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
*/
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
post("/api/v3/repos/:owner/:repo/statuses/:sha")(writableUsersOnly { repository =>
(for{
ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
@@ -438,7 +479,7 @@ trait ApiControllerBase extends ControllerBase {
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -454,7 +495,7 @@ trait ApiControllerBase extends ControllerBase {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -479,11 +520,11 @@ trait ApiControllerBase extends ControllerBase {
} yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound
}) getOrElse NotFound()
})
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -191,6 +191,7 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
case agent if agent.contains("Win") => "windows"
case _ => null
}
val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null
/**
* Get object from cache.
@@ -244,4 +245,13 @@ trait AccountManagementControllerBase extends ControllerBase {
.map { _ => "Mail address is already registered." }
}
val allReservedNames = Set("git", "admin", "upload", "api")
protected def reservedNames(): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
Some(s"${value} is reserved")
} else {
None
}
}
}

View File

@@ -15,20 +15,7 @@ trait DashboardControllerBase extends ControllerBase {
with UsersAuthenticator =>
get("/dashboard/issues")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
case _ => searchIssues("created_by")
}
} getOrElse {
searchIssues("created_by")
}
searchIssues("created_by")
})
get("/dashboard/issues/assigned")(usersOnly {
@@ -44,20 +31,7 @@ trait DashboardControllerBase extends ControllerBase {
})
get("/dashboard/pulls")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
case _ => searchPullRequests("created_by")
}
} getOrElse {
searchPullRequests("created_by")
}
searchPullRequests("created_by")
})
get("/dashboard/pulls/created_by")(usersOnly {
@@ -73,14 +47,7 @@ trait DashboardControllerBase extends ControllerBase {
})
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = session.putAndGet(key, if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, Map[String, Int]())
}
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
val condition = IssueSearchCondition(request)
filter match {
case "assigned" => condition.copy(assigned = Some(Some(userName)), author = None, mentioned = None)

View File

@@ -48,7 +48,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
// Check whether logged-in user is collaborator
collaboratorsOnly(owner, repository, loginAccount){
execute({ (file, fileId) =>
val fileName = file.getName
val fileName = file.getName
LockUtil.lock(s"${owner}/${repository}/wiki") {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
val builder = DirCache.newInCore.builder()
@@ -75,19 +75,14 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
}
}, FileUtil.isUploadableType)
}
} getOrElse BadRequest
} getOrElse BadRequest()
}
post("/import") {
import JDBCUtil._
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
execute({ (file, fileId) =>
if(file.getName.endsWith(".xml")){
import JDBCUtil._
val conn = request2Session(request).conn
conn.importAsXML(file.getInputStream)
} else {
throw new RuntimeException("Import is available for only the XML file.")
}
request2Session(request).conn.importAsSQL(file.getInputStream)
}, _ => true)
}
redirect("/admin/data")
@@ -98,7 +93,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
loginAccount match {
case x if(x.isAdmin) => action
case x if(getCollaborators(owner, repository).contains(x.userName)) => action
case _ => BadRequest
case _ => BadRequest()
}
}
@@ -106,10 +101,9 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
Ok(fileId)
}
case _ => BadRequest
case _ => BadRequest()
}
}

View File

@@ -5,9 +5,9 @@ import gitbucket.core.model.Account
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, StringUtil, UsersAuthenticator}
import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok
class IndexController extends IndexControllerBase
@@ -36,23 +36,11 @@ trait IndexControllerBase extends ControllerBase {
get("/"){
val loginAccount = context.loginAccount
if(loginAccount.isEmpty) {
gitbucket.core.html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
} else {
val loginUserName = loginAccount.get.userName
val loginUserGroups = getGroupsByUserName(loginUserName)
var visibleOwnerSet : Set[String] = Set(loginUserName)
visibleOwnerSet ++= loginUserGroups
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
context.loginAccount.map { account =>
val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName)
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet), Nil, getUserRepositories(account.userName, withoutPhysicalInfo = true))
}.getOrElse {
gitbucket.core.html.index(getRecentActivities(), getVisibleRepositories(None, withoutPhysicalInfo = true), Nil)
}
}
@@ -81,6 +69,15 @@ trait IndexControllerBase extends ControllerBase {
xml.feed(getRecentActivities())
}
get("/sidebar-collapse"){
if(params("collapse") == "true"){
session.setAttribute("sidebar-collapse", "true")
} else {
session.setAttribute("sidebar-collapse", null)
}
Ok()
}
/**
* Set account information into HttpSession and redirect.
*/
@@ -108,18 +105,29 @@ trait IndexControllerBase extends ControllerBase {
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
val user = params("user").toBoolean
val group = params("group").toBoolean
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray)
Map("options" -> (
getAllUsers(false)
.withFilter { t => (user, group) match {
case (true, true) => true
case (true, false) => !t.isGroupAccount
case (false, true) => t.isGroupAccount
case (false, false) => false
}}.map { t => t.userName }
))
)
})
/**
* JSON API for checking user existence.
* JSON API for checking user or group existence.
* Returns a single string which is any of "group", "user" or "".
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).map { account =>
if(params.get("userOnly").isDefined) !account.isGroupAccount else true
} getOrElse false
if(account.isGroupAccount) "group" else "user"
} getOrElse ""
})
// TODO Move to RepositoryViwerController?

View File

@@ -2,24 +2,24 @@ package gitbucket.core.controller
import gitbucket.core.issues.html
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
@@ -67,142 +67,147 @@ trait IssuesControllerBase extends ControllerBase {
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
isEditable(repository),
isManageable(repository),
repository)
} getOrElse NotFound
} getOrElse NotFound()
}
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
defining(repository.owner, repository.name){ case (owner, name) =>
html.create(
getAssignableUserNames(owner, name),
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
isManageable(repository),
repository)
}
}
} else Unauthorized()
})
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount)
val userName = context.loginAccount.get.userName
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
defining(repository.owner, repository.name){ case (owner, name) =>
val manageable = isManageable(repository)
val userName = context.loginAccount.get.userName
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None)
// insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content,
if (manageable) form.assignedUserName else None,
if (manageable) form.milestoneId else None)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
// insert labels
if (manageable) {
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
}
}
}
}
}
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
}
redirect(s"/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
}
} else Unauthorized()
})
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
if(isEditableContent(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment
createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
if(isEditableContent(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
}
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
}
} getOrElse NotFound
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
if(isEditableContent(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
if(isEditableContent(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
if(isEditableContent(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect {
case t if t == "html" => html.editissue(x.content, x.issueId, repository)
} getOrElse {
@@ -218,18 +223,18 @@ trait IssuesControllerBase extends ControllerBase {
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName)
hasWritePermission = true
)
)
)
}
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
if(isEditableContent(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
} getOrElse {
@@ -244,51 +249,51 @@ trait IssuesControllerBase extends ControllerBase {
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
hasWritePermission = true
)
)
)
}
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository =>
val labelNames = params("labelNames").split(",")
val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
html.labellist(labels)
})
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated")
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound
} getOrElse NotFound()
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
defining(params.get("value")){ action =>
action match {
case Some("open") => executeBatch(repository) { issueId =>
@@ -306,17 +311,17 @@ trait IssuesControllerBase extends ControllerBase {
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository =>
params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
defining(assignedUserName("value")){ value =>
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
@@ -324,7 +329,7 @@ trait IssuesControllerBase extends ControllerBase {
}
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
defining(milestoneId("value")){ value =>
executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value)
@@ -340,15 +345,12 @@ trait IssuesControllerBase extends ControllerBase {
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound
}) getOrElse NotFound()
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
params("from") match {
@@ -359,37 +361,51 @@ trait IssuesControllerBase extends ControllerBase {
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
val page = IssueSearchCondition.page(request)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null || q.trim.isEmpty){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
}
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
val condition = IssueSearchCondition(request)
html.list(
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getAssignableUserNames(owner, repoName),
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
isEditable(repository),
isManageable(repository))
}
}
/**
* Tests whether an logged-in user can manage issues.
*/
private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
}
/**
* Tests whether an logged-in user can post issues.
*/
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.options.issuesOption match {
case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
case "DISABLE" => false
}
}
/**
* Tests whether an issue or a comment is editable by a logged-in user.
*/
private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = {
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}
}

View File

@@ -2,7 +2,7 @@ package gitbucket.core.controller
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
@@ -10,11 +10,11 @@ import org.scalatra.Ok
class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReferrerAuthenticator with WritableUsersAuthenticator
trait LabelsControllerBase extends ControllerBase {
self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReferrerAuthenticator with WritableUsersAuthenticator =>
case class LabelForm(labelName: String, color: String)
@@ -29,40 +29,40 @@ trait LabelsControllerBase extends ControllerBase {
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository =>
html.edit(None, repository)
})
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(writableUsersOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository)
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(writableUsersOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok()
})

View File

@@ -2,17 +2,17 @@ package gitbucket.core.controller
import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReferrerAuthenticator with WritableUsersAuthenticator
trait MilestonesControllerBase extends ControllerBase {
self: MilestonesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReferrerAuthenticator with WritableUsersAuthenticator =>
case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date])
@@ -27,58 +27,58 @@ trait MilestonesControllerBase extends ControllerBase {
params.getOrElse("state", "open"),
getMilestonesWithIssueCount(repository.owner, repository.name),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
})
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
get("/:owner/:repository/issues/milestones/new")(writableUsersOnly {
html.edit(None, _)
})
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/issues/milestones/new", milestoneForm)(writableUsersOnly { (form, repository) =>
createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.map{ milestoneId =>
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableUsersOnly { (form, repository) =>
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(writableUsersOnly { repository =>
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
} getOrElse NotFound()
})
}

View File

@@ -6,6 +6,7 @@ import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
@@ -14,28 +15,26 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.helpers
import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory
import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitsService with ActivityService with WebHookPullRequestService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with CommitStatusService with MergeService with ProtectedBranchService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
with CommitStatusService with MergeService with ProtectedBranchService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
@@ -94,17 +93,18 @@ trait PullRequestsControllerBase extends ControllerBase {
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
isEditable(repository),
isManageable(repository),
repository,
flash.toMap.map(f => f._1 -> f._2.toString))
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
@@ -115,7 +115,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount)
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict,
@@ -125,7 +125,7 @@ trait PullRequestsControllerBase extends ControllerBase {
needStatusCheck = context.loginAccount.map{ u =>
branchProtection.needStatusCheck(u.userName)
}.getOrElse(true),
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
hasUpdatePermission = hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
context.loginAccount.map{ u =>
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
}.getOrElse(false),
@@ -138,10 +138,10 @@ trait PullRequestsControllerBase extends ControllerBase {
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/pull/:id/delete/*")(writableUsersOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
@@ -153,27 +153,27 @@ trait PullRequestsControllerBase extends ControllerBase {
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository =>
post("/:owner/:repository/pull/:id/update_branch")(writableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasWritePermission(owner, name, context.loginAccount)
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else {
val repository = getRepository(owner, name).get
LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch
}else{
} else {
s"${pullreq.userName}:${pullreq.branch}"
}
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
@@ -187,11 +187,10 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// after update branch
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
commits.foreach{ commit =>
commits.foreach { commit =>
if(!existIds.contains(commit.id)){
createIssueComment(owner, name, commit)
}
@@ -220,12 +219,13 @@ trait PullRequestsControllerBase extends ControllerBase {
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}
}) getOrElse NotFound
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}) getOrElse NotFound()
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/pull/:id/merge", mergeForm)(writableUsersOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
val name = repository.name
@@ -273,7 +273,7 @@ trait PullRequestsControllerBase extends ControllerBase {
}
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
@@ -290,7 +290,7 @@ trait PullRequestsControllerBase extends ControllerBase {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
} getOrElse NotFound()
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
@@ -374,8 +374,8 @@ trait PullRequestsControllerBase extends ControllerBase {
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount),
(getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted,
hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount),
getAssignableUserNames(originRepository.owner, originRepository.name),
getMilestones(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name)
)
@@ -386,10 +386,10 @@ trait PullRequestsControllerBase extends ControllerBase {
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}")
}
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
@@ -416,67 +416,71 @@ trait PullRequestsControllerBase extends ControllerBase {
}
html.mergecheck(conflict)
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount)
val loginUserName = context.loginAccount.get.userName
val manageable = isManageable(repository)
val editable = isEditable(repository)
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = if(writable) form.assignedUserName else None,
milestoneId = if(writable) form.milestoneId else None,
isPullRequest = true)
if(editable) {
val loginUserName = context.loginAccount.get.userName
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = if (manageable) form.assignedUserName else None,
milestoneId = if (manageable) form.milestoneId else None,
isPullRequest = true)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// insert labels
if (manageable) {
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
}
}
}
}
}
// fetch requested branch
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// fetch requested branch
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// record activity
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
// record activity
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
getIssue(owner, name, issueId.toString) foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
getIssue(owner, name, issueId.toString) foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
}
}
redirect(s"/${owner}/${name}/pull/${issueId}")
redirect(s"/${owner}/${name}/pull/${issueId}")
} else Unauthorized()
}
})
@@ -516,31 +520,43 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
val page = IssueSearchCondition.page(request)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
val condition = IssueSearchCondition(request)
gitbucket.core.issues.html.list(
"pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getAssignableUserNames(owner, repoName),
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
isEditable(repository),
isManageable(repository))
}
/**
* Tests whether an logged-in user can manage pull requests.
*/
private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
}
/**
* Tests whether an logged-in user can post pull requests.
*/
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.options.issuesOption match {
case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
case "DISABLE" => false
}
}
}

View File

@@ -31,22 +31,22 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repositoryName: String,
description: Option[String],
isPrivate: Boolean,
enableIssues: Boolean,
issuesOption: String,
externalIssuesUrl: Option[String],
enableWiki: Boolean,
allowWikiEditing: Boolean,
externalWikiUrl: Option[String]
wikiOption: String,
externalWikiUrl: Option[String],
allowFork: Boolean
)
val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))),
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type" , boolean())),
"enableIssues" -> trim(label("Enable Issues" , boolean())),
"issuesOption" -> trim(label("Issues Option" , text(required, featureOption))),
"externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
"enableWiki" -> trim(label("Enable Wiki" , boolean())),
"allowWikiEditing" -> trim(label("Allow Wiki Editing" , boolean())),
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200)))))
"wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))),
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))),
"allowFork" -> trim(label("Allow Forking" , boolean()))
)(OptionsForm.apply)
// for default branch
@@ -56,12 +56,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
)(DefaultBranchForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String)
val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply)
// // for collaborator addition
// case class CollaboratorForm(userName: String)
//
// val collaboratorForm = mapping(
// "userName" -> trim(label("Username", text(required, collaborator)))
// )(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
@@ -107,11 +107,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate,
form.enableIssues,
form.issuesOption,
form.externalIssuesUrl,
form.enableWiki,
form.allowWikiEditing,
form.externalWikiUrl
form.wikiOption,
form.externalWikiUrl,
form.allowFork
)
// Change repository name
if(repository.name != form.repositoryName){
@@ -175,22 +175,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
addCollaborator(repository.owner, repository.name, form.userName)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
post("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
val collaborators = params("collaborators")
removeCollaborators(repository.owner, repository.name)
collaborators.split(",").withFilter(_.nonEmpty).map { collaborator =>
val userName :: role :: Nil = collaborator.split(":").toList
addCollaborator(repository.owner, repository.name, userName, role)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
@@ -297,7 +287,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.edithooks(webhook, events, repository, flash.get("info"), false)
} getOrElse NotFound
} getOrElse NotFound()
})
/**
@@ -394,20 +384,20 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
/**
* Provides Constraint to validate the collaborator name.
*/
private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None
}
}
// /**
// * Provides Constraint to validate the collaborator name.
// */
// private def collaborator: Constraint = new Constraint(){
// override def validate(name: String, value: String, messages: Messages): Option[String] =
// getAccountByUserName(value) match {
// case None => Some("User does not exist.")
//// case Some(x) if(x.isGroupAccount)
//// => Some("User does not exist.")
// case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
// => Some(value + " is repository owner.") // TODO also group members?
// case _ => None
// }
// }
/**
* Duplicate check for the rename repository name.
@@ -421,6 +411,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
/**
*
*/
private def featureOption: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
}
/**
* Provides Constraint to validate the repository transfer user.
*/

View File

@@ -31,7 +31,7 @@ import org.scalatra._
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
/**
@@ -39,7 +39,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
@@ -110,16 +110,21 @@ trait RepositoryViewerControllerBase extends ControllerBase {
enableLineBreaks = params("enableLineBreaks").toBoolean,
enableTaskList = params("enableTaskList").toBoolean,
enableAnchor = false,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
})
/**
* Displays the file list of the repository root and the default branch.
*/
get("/:owner/:repository")(referrersOnly {
fileList(_)
})
get("/:owner/:repository") {
params.get("go-get") match {
case Some("1") => defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound()
}
case _ => referrersOnly(fileList(_))
}
}
/**
* Displays the file list of the specified path and branch.
@@ -146,13 +151,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound()
}
}
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/new/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
@@ -160,7 +165,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
protectedBranch)
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
@@ -172,11 +177,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId),
protectedBranch)
} getOrElse NotFound
} getOrElse NotFound()
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/remove/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
@@ -185,11 +190,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val paths = path.split("/")
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
} getOrElse NotFound()
}
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
commitFile(
repository = repository,
branch = form.branch,
@@ -206,7 +211,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}")
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
commitFile(
repository = repository,
branch = form.branch,
@@ -227,7 +232,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
@@ -245,7 +250,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loader.copyTo(response.outputStream)
()
}
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -265,15 +270,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
} getOrElse NotFound
} getOrElse NotFound()
} else {
html.blob(id, repository, path.split("/").toList,
JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission(repository.owner, repository.name, context.loginAccount),
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame")
}
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -324,12 +329,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, false),
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}
}
}
} catch {
case e:MissingObjectException => NotFound
case e:MissingObjectException => NotFound()
}
})
@@ -353,7 +358,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
repository = repository
)
})
@@ -369,7 +374,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
@@ -388,12 +393,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
hasWritePermission = true
)
))
}
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
@@ -402,8 +407,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(isEditable(owner, name, comment.commentedUserName)){
updateCommitComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
@@ -412,8 +417,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteCommitComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
}
})
@@ -432,13 +437,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
html.branches(branches, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
})
/**
* Creates a branch.
*/
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
post("/:owner/:repository/branches")(writableUsersOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -456,7 +461,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
get("/:owner/:repository/delete/*")(writableUsersOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
@@ -484,23 +489,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
archiveRepository(name, ".zip", repository)
case name if name.endsWith(".tar.gz") =>
archiveRepository(name, ".tar.gz", repository)
case _ => BadRequest
case _ => BadRequest()
}
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user
repository)
if(repository.repository.options.allowFork) {
html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user
repository)
} else BadRequest()
})
/**
@@ -511,7 +518,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId =>
html.find(ref, treeId, repository)
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -540,7 +547,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
html.guide(repository, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
} else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// get specified commit
@@ -562,11 +569,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
files, readme, hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
} getOrElse NotFound()
}
}
}
@@ -586,14 +593,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) =>
val permission = JGitUtil.processTree(git, headTip){ (path, tree) =>
// Add all entries except the editing file
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
// Retrieve permission if file exists to keep it
oldPath.collect { case x if x == path => tree.getEntryFileMode.getBits }
}.flatten.headOption
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
builder.add(JGitUtil.createDirCacheEntry(newPath,
permission.map { bits => FileMode.fromBits(bits) } getOrElse FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
@@ -616,8 +627,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
updatePullRequests(repository.owner, repository.name, branch)
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
// create issue comment by commit message
createIssueComment(repository.owner, repository.name, commitInfo)
// close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
@@ -677,7 +691,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace()

View File

@@ -13,9 +13,7 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._
import io.github.gitbucket.scalatra.forms._
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import org.apache.commons.io.{FileUtils, IOUtils}
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.scalatra.i18n.Messages
class SystemSettingsController extends SystemSettingsControllerBase
@@ -105,7 +103,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
@@ -127,7 +125,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
@@ -176,11 +174,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
get("/admin/plugins")(adminOnly {
val manager = new JDBCVersionManager(request2Session(request).conn)
val plugins = PluginRegistry().getPlugins().map { plugin =>
(plugin, manager.getCurrentVersion(plugin.pluginId))
}
html.plugins(plugins)
html.plugins(PluginRegistry().getPlugins())
})
@@ -239,7 +233,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/admin/users/_newgroup")(adminOnly {
@@ -285,19 +279,19 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} else {
// Update GROUP_MEMBER
updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
// // Update COLLABORATOR for group repositories
// getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
// removeCollaborators(form.groupName, repositoryName)
// members.foreach { case (userName, isManager) =>
// addCollaborator(form.groupName, repositoryName, userName)
// }
// }
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -309,12 +303,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/export")(adminOnly {
import gitbucket.core.util.JDBCUtil._
val session = request2Session(request)
val file = if(params("type") == "sql"){
session.conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
} else {
session.conn.exportAsXML(request.getParameterValues("tableNames").toSeq)
}
val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)

View File

@@ -14,10 +14,10 @@ import org.scalatra.i18n.Messages
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
with ReadableUsersAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
self: WikiService with RepositoryService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
@@ -62,7 +62,7 @@ trait WikiControllerBase extends ControllerBase {
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository)
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository))
case Left(_) => NotFound()
}
}
@@ -87,7 +87,7 @@ trait WikiControllerBase extends ControllerBase {
}
})
get("/:owner/:repository/wiki/:page/_revert/:commitId")(referrersOnly { repository =>
get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository =>
if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -101,7 +101,7 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized()
})
get("/:owner/:repository/wiki/_revert/:commitId")(referrersOnly { repository =>
get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository =>
if(isEditable(repository)){
val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -114,14 +114,14 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized()
})
get("/:owner/:repository/wiki/:page/_edit")(referrersOnly { repository =>
get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository =>
if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page"))
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
} else Unauthorized()
})
post("/:owner/:repository/wiki/_edit", editForm)(referrersOnly { (form, repository) =>
post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(
@@ -146,13 +146,13 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized()
})
get("/:owner/:repository/wiki/_new")(referrersOnly { repository =>
get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository =>
if(isEditable(repository)){
html.edit("", None, repository)
} else Unauthorized()
})
post("/:owner/:repository/wiki/_new", newForm)(referrersOnly { (form, repository) =>
post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
@@ -170,7 +170,7 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized()
})
get("/:owner/:repository/wiki/:page/_delete")(referrersOnly { repository =>
get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository =>
if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page"))
@@ -182,7 +182,7 @@ trait WikiControllerBase extends ControllerBase {
}
} else Unauthorized()
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository))
})
@@ -190,7 +190,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => html.history(None, logs, repository)
case Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository))
case Left(_) => NotFound()
}
}
@@ -201,7 +201,7 @@ trait WikiControllerBase extends ControllerBase {
getFileContent(repository.owner, repository.name, path).map { bytes =>
RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound
} getOrElse NotFound()
})
private def unique: Constraint = new Constraint(){
@@ -240,9 +240,13 @@ trait WikiControllerBase extends ControllerBase {
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean =
repository.repository.allowWikiEditing || (
hasWritePermission(repository.owner, repository.name, context.loginAccount)
)
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.options.wikiOption match {
case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
case "DISABLE" => false
}
}
}

View File

@@ -7,7 +7,8 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
val collaboratorName = column[String]("COLLABORATOR_NAME")
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
val role = column[String]("ROLE")
def * = (userName, repositoryName, collaboratorName, role) <> (Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName === collaborator.bind)
@@ -17,5 +18,23 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
case class Collaborator(
userName: String,
repositoryName: String,
collaboratorName: String
collaboratorName: String,
role: String
)
sealed abstract class Role(val name: String)
object Role {
object ADMIN extends Role("ADMIN")
object DEVELOPER extends Role("DEVELOPER")
object GUEST extends Role("GUEST")
// val values: Vector[Permission] = Vector(ADMIN, WRITE, READ)
//
// private val map: Map[String, Permission] = values.map(enum => enum.name -> enum).toMap
//
// def apply(name: String): Permission = map(name)
//
// def valueOf(name: String): Option[Permission] = map.get(name)
}

View File

@@ -7,24 +7,61 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
lazy val Repositories = TableQuery[Repositories]
class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate {
val isPrivate = column[Boolean]("PRIVATE")
val description = column[String]("DESCRIPTION")
val defaultBranch = column[String]("DEFAULT_BRANCH")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
val originUserName = column[String]("ORIGIN_USER_NAME")
val isPrivate = column[Boolean]("PRIVATE")
val description = column[String]("DESCRIPTION")
val defaultBranch = column[String]("DEFAULT_BRANCH")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
val originUserName = column[String]("ORIGIN_USER_NAME")
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
val parentUserName = column[String]("PARENT_USER_NAME")
val parentUserName = column[String]("PARENT_USER_NAME")
val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
val enableIssues = column[Boolean]("ENABLE_ISSUES")
val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL")
val enableWiki = column[Boolean]("ENABLE_WIKI")
val allowWikiEditing = column[Boolean]("ALLOW_WIKI_EDITING")
val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch,
registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?,
enableIssues, externalIssuesUrl.?, enableWiki, allowWikiEditing, externalWikiUrl.?) <> (Repository.tupled, Repository.unapply)
val issuesOption = column[String]("ISSUES_OPTION")
val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL")
val wikiOption = column[String]("WIKI_OPTION")
val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL")
val allowFork = column[Boolean]("ALLOW_FORK")
def * = (
(userName, repositoryName, isPrivate, description.?, defaultBranch,
registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?),
(issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork)
).shaped <> (
{ case (repository, options) =>
Repository(
repository._1,
repository._2,
repository._3,
repository._4,
repository._5,
repository._6,
repository._7,
repository._8,
repository._9,
repository._10,
repository._11,
repository._12,
RepositoryOptions.tupled.apply(options)
)
}, { (r: Repository) =>
Some(((
r.userName,
r.repositoryName,
r.isPrivate,
r.description,
r.defaultBranch,
r.registeredDate,
r.updatedDate,
r.lastActivityDate,
r.originUserName,
r.originRepositoryName,
r.parentUserName,
r.parentRepositoryName
),(
RepositoryOptions.unapply(r.options).get
)))
})
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
@@ -43,9 +80,13 @@ case class Repository(
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String],
enableIssues: Boolean,
externalIssuesUrl: Option[String],
enableWiki: Boolean,
allowWikiEditing: Boolean,
externalWikiUrl: Option[String]
options: RepositoryOptions
)
case class RepositoryOptions(
issuesOption: String,
externalIssuesUrl: Option[String],
wikiOption: String,
externalWikiUrl: Option[String],
allowFork: Boolean
)

View File

@@ -13,6 +13,7 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.apache.commons.codec.binary.{Base64, StringUtils}
import org.slf4j.LoggerFactory
@@ -161,6 +162,8 @@ object PluginRegistry {
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
@@ -173,6 +176,13 @@ object PluginRegistry {
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if(databaseVersion != pluginVersion){
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
@@ -185,7 +195,7 @@ object PluginRegistry {
} catch {
case e: Throwable => {
logger.error(s"Error during plugin initialization", e)
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
}
}
}

View File

@@ -181,12 +181,11 @@ trait AccountService {
def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
GroupMembers.filter(_.userName === userName.bind).delete
Collaborators.filter(_.collaboratorName === userName.bind).delete
Repositories.filter(_.userName === userName.bind).delete
}
def getGroupNames(userName: String)(implicit s: Session): List[String] = {
List(userName) ++
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct
}
}

View File

@@ -13,7 +13,7 @@ trait HandleCommentService {
with WebHookService with WebHookIssueCommentService with WebHookPullRequestService =>
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
* @see [[https://github.com/gitbucket/gitbucket/wiki/CommentAction]]
*/
def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
(implicit context: Context, s: Session) = {
@@ -54,18 +54,20 @@ trait HandleCommentService {
// call web hooks
action match {
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
case Some(act) => val webHookAction = act match {
case "open" => "opened"
case "reopen" => "reopened"
case "close" => "closed"
case _ => act
}
if(issue.isPullRequest){
case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
case Some(act) => {
val webHookAction = act match {
case "open" => "opened"
case "reopen" => "reopened"
case "close" => "closed"
case _ => act
}
if (issue.isPullRequest) {
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
}
}
}
// notifications

View File

@@ -14,7 +14,7 @@ import Q.interpolation
trait IssuesService {
self: AccountService =>
self: AccountService with RepositoryService =>
import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
@@ -163,66 +163,62 @@ trait IssuesService {
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.leftJoin (IssueLabels) .on { case (((t1, t2), i), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case ((((t1, t2), i), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.leftJoin (Milestones) .on { case (((((t1, t2), i), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.sortBy { case (((((t1, t2), i), t3), t4), t5) => i asc }
.map { case (((((t1, t2), i), t3), t4), t5) => (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) }
.list
.splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId }
val status = getCommitStatues(result.map(_.head._1).map(is => (is.userName, is.repositoryName, is.issueId)))
result.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount,
status.get(issue.userName, issue.repositoryName, issue.issueId))
}} toList
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount,
status.get(issue.userName, issue.repositoryName, issue.issueId))
}} toList
}
/** for api
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
.innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName }
.innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName }
.map { case (((((t1, t2), t3), t4), t5), t6) =>
(t1, t5, t2.commentCount, t3, t4, t6)
}
.innerJoin(PullRequests).on { case (((t1, t2), i), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.innerJoin(Repositories).on { case ((((t1, t2), i), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
.innerJoin(Accounts).on { case (((((t1, t2), i), t3), t4), t5) => t5.userName === t1.openedUserName }
.innerJoin(Accounts).on { case ((((((t1, t2), i), t3), t4), t5), t6) => t6.userName === t4.userName }
.sortBy { case ((((((t1, t2), i), t3), t4), t5), t6) => i asc }
.map { case ((((((t1, t2), i), t3), t4), t5), t6) => (t1, t5, t2.commentCount, t3, t4, t6) }
.list
}
private def searchIssueQueryBase(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: Seq[(String, String)])
(implicit s: Session) =
(implicit s: Session) =
searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => t2.commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => t1.issueId desc }
.sortBy { case (t1, t2) =>
(condition.sort match {
case "created" => t1.registeredDate
case "comments" => t2.commentCount
case "updated" => t1.updatedDate
}) match {
case sort => condition.direction match {
case "asc" => sort asc
case "desc" => sort desc
}
}
.drop(offset).take(limit)
}
.drop(offset).take(limit).zipWithIndex
/**
@@ -437,6 +433,11 @@ trait IssuesService {
}
}
def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = {
(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)) :::
(if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).distinct.sorted
}
}
object IssuesService {
@@ -518,50 +519,6 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
}
/**
* Restores IssueSearchCondition instance from filter query.
*/
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
val conditions = filter.split("[  \t]+").flatMap { x =>
x.split(":") match {
case Array(key, value) => Some((key, value))
case _ => None
}
}.groupBy(_._1).map { case (key, values) =>
key -> values.map(_._2).toSeq
}
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
case "created-asc" => ("created" , "asc" )
case "comments-desc" => ("comments", "desc")
case "comments-asc" => ("comments", "asc" )
case "updated-desc" => ("comments", "desc")
case "updated-asc" => ("comments", "asc" )
case _ => ("created" , "desc")
}
IssueSearchCondition(
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
conditions.get("milestone").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => Some(Some(x))
},
conditions.get("author").flatMap(_.headOption),
conditions.get("assignee").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => Some(Some(x))
},
conditions.get("mentions").flatMap(_.headOption),
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
sort,
direction,
conditions.get("visibility").flatMap(_.headOption),
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
)
}
/**
* Restores IssueSearchCondition instance from request parameters.
*/

View File

@@ -41,7 +41,7 @@ trait MilestonesService {
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
val counts = Issues
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) }
.filter { t => t.byRepository(owner, repository) && (t.milestoneId.? isDefined) }
.groupBy { t => t.milestoneId -> t.closed }
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
.toMap
@@ -52,6 +52,6 @@ trait MilestonesService {
}
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
Milestones.filter(_.byRepository(owner, repository)).sortBy(t => (t.dueDate.asc, t.closedDate.desc, t.milestoneId.desc)).list
}

View File

@@ -21,12 +21,12 @@ trait RepositoryCreationService {
// Insert to the database at first
insertRepository(name, owner, description, isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName)
}
}
// // Add collaborators for group repository
// if(ownerAccount.isGroupAccount){
// getGroupMembers(owner).foreach { member =>
// addCollaborator(owner, name, member.userName)
// }
// }
// Insert default labels
insertDefaultLabels(owner, name)

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Collaborator, Repository, Account}
import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Role}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
@@ -37,11 +37,13 @@ trait RepositoryService { self: AccountService =>
originRepositoryName = originRepositoryName,
parentUserName = parentUserName,
parentRepositoryName = parentRepositoryName,
enableIssues = true,
externalIssuesUrl = None,
enableWiki = true,
allowWikiEditing = true,
externalWikiUrl = None
options = RepositoryOptions(
issuesOption = "PUBLIC", // TODO DISABLE for the forked repository?
externalIssuesUrl = None,
wikiOption = "PUBLIC", // TODO DISABLE for the forked repository?
externalWikiUrl = None,
allowFork = true
)
)
IssueId insert (userName, repositoryName, 0)
@@ -121,11 +123,8 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName
)) :_*)
if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
}
// TODO Drop transfered owner from collaborators?
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
Activities.filter { t =>
@@ -224,7 +223,7 @@ trait RepositoryService { self: AccountService =>
}
/**
* Returns the repositories without private repository that user does not have access right.
* Returns the repositories except private repository that user does not have access right.
* Include public repository, private own repository and private but collaborator repository.
*
* @param userName the user name of collaborator
@@ -233,8 +232,10 @@ trait RepositoryService { self: AccountService =>
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
Repositories.filter { t1 =>
(t1.isPrivate === false.bind) ||
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
(t1.userName === userName.bind) || (t1.userName in (GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) &&
((t2.collaboratorName === userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === userName.bind).map(_.groupName)))
} exists)
}.sortBy(_.lastActivityDate desc).map{ t =>
(t.userName, t.repositoryName)
}.list
@@ -243,8 +244,10 @@ trait RepositoryService { self: AccountService =>
def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName === userName.bind) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
(t1.userName === userName.bind) || (t1.userName in (GroupMembers.filter(_.userName === userName.bind).map(_.groupName))) ||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) &&
((t2.collaboratorName === userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === userName.bind).map(_.groupName)))
} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
@@ -279,8 +282,13 @@ trait RepositoryService { self: AccountService =>
case Some(x) if(x.isAdmin) => Repositories
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) ||
(Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
Repositories filter { t =>
(t.isPrivate === false.bind) || (t.userName === x.userName) ||
(t.userName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName)) ||
(Collaborators.filter { t2 =>
t2.byRepository(t.userName, t.repositoryName) &&
((t2.collaboratorName === x.userName.bind) || (t2.collaboratorName in GroupMembers.filter(_.userName === x.userName.bind).map(_.groupName)))
} exists)
}
// for Guests
case None => Repositories filter(_.isPrivate === false.bind)
@@ -320,11 +328,12 @@ trait RepositoryService { self: AccountService =>
*/
def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], isPrivate: Boolean,
enableIssues: Boolean, externalIssuesUrl: Option[String],
enableWiki: Boolean, allowWikiEditing: Boolean, externalWikiUrl: Option[String])(implicit s: Session): Unit =
issuesOption: String, externalIssuesUrl: Option[String],
wikiOption: String, externalWikiUrl: Option[String],
allowFork: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.isPrivate, r.enableIssues, r.externalIssuesUrl.?, r.enableWiki, r.allowWikiEditing, r.externalWikiUrl.?, r.updatedDate) }
.update (description, isPrivate, enableIssues, externalIssuesUrl, enableWiki, allowWikiEditing, externalWikiUrl, currentDate)
.map { r => (r.description.?, r.isPrivate, r.issuesOption, r.externalIssuesUrl.?, r.wikiOption, r.externalWikiUrl.?, r.allowFork, r.updatedDate) }
.update (description, isPrivate, issuesOption, externalIssuesUrl, wikiOption, externalWikiUrl, allowFork, currentDate)
def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
defaultBranch: String)(implicit s: Session): Unit =
@@ -333,49 +342,64 @@ trait RepositoryService { self: AccountService =>
.update (defaultBranch)
/**
* Add collaborator to the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param collaboratorName the collaborator name
* Add collaborator (user or group) to the repository.
*/
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
/**
* Remove collaborator from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param collaboratorName the collaborator name
*/
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, role: String)(implicit s: Session): Unit =
Collaborators insert Collaborator(userName, repositoryName, collaboratorName, role)
/**
* Remove all collaborators from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
*/
def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/**
* Returns the list of collaborators name which is sorted with ascending order.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @return the list of collaborators name
* Returns the list of collaborators name (user name or group name) which is sorted with ascending order.
*/
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[(Collaborator, Boolean)] =
Collaborators
.innerJoin(Accounts).on(_.collaboratorName === _.userName)
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
.map { case (t1, t2) => (t1, t2.groupAccount) }
.sortBy { case (t1, t2) => t1.collaboratorName }
.list
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
/**
* Returns the list of all collaborator name and permission which is sorted with ascending order.
* If a group is added as a collaborator, this method returns users who are belong to that group.
*/
def getCollaboratorUserNames(userName: String, repositoryName: String, filter: Seq[Role] = Nil)(implicit s: Session): List[String] = {
val q1 = Collaborators
.innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) }
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
.map { case (t1, t2) => (t1.collaboratorName, t1.role) }
val q2 = Collaborators
.innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) }
.innerJoin(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName }
.filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) }
.map { case ((t1, t2), t3) => (t3.userName, t1.role) }
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true
case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true
case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) => true
case _ => false
}
}
def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true
case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST)).contains(a.userName)) => true
case _ => false
}
}

View File

@@ -11,7 +11,7 @@ import Implicits.request2Session
* It may be called many times in one request, so each method stores
* its result into the cache which available during a request.
*/
trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
trait RequestCache extends SystemSettingsService with AccountService with IssuesService with RepositoryService {
private implicit def context2Session(implicit context: Context): Session =
request2Session(context.request)

View File

@@ -73,7 +73,7 @@ trait SystemSettingsService {
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Gravatar, false),
getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false),

View File

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

View File

@@ -84,7 +84,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield if(isUpdating || repository.repository.isPrivate){
if(hasWritePermission(repository.owner, repository.name, Some(account))){
if(hasDeveloperRole(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
true
} else false

View File

@@ -17,6 +17,7 @@ import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import akka.actor.{Actor, Props, ActorSystem}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import scala.collection.JavaConverters._
/**
* Initialize GitBucket system.
@@ -80,8 +81,14 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
if(currentVersion == "4.0"){
val databaseVersion = if(currentVersion == "4.0"){
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
"4.0.0"
} else currentVersion
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
if(databaseVersion != gitbucketVersion){
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
}
// Load plugins

View File

@@ -52,6 +52,13 @@ object Database {
config.setJdbcUrl(DatabaseConfig.url)
config.setUsername(DatabaseConfig.user)
config.setPassword(DatabaseConfig.password)
config.setAutoCommit(false)
DatabaseConfig.connectionTimeout.foreach(config.setConnectionTimeout)
DatabaseConfig.idleTimeout.foreach(config.setIdleTimeout)
DatabaseConfig.maxLifetime.foreach(config.setMaxLifetime)
DatabaseConfig.minimumIdle.foreach(config.setMinimumIdle)
DatabaseConfig.maximumPoolSize.foreach(config.setMaximumPoolSize)
logger.debug("load database connection pool")
new HikariDataSource(config)
}

View File

@@ -13,7 +13,7 @@ import ControlUtil._
import org.eclipse.jgit.api.Git
import Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand
import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand {
@@ -36,7 +36,7 @@ abstract class GitCommand extends Command with SessionAware {
override def run(): Unit = {
authUser match {
case Some(authUser) =>
Database() withSession { implicit session =>
Database() withTransaction { implicit session =>
try {
runTask(authUser)
callback.onExit(0)
@@ -92,7 +92,7 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean =
getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
case Some(account) => hasDeveloperRole(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false
}

View File

@@ -7,12 +7,12 @@ import gitbucket.core.service.SshKeyService
import gitbucket.core.servlet.Database
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.common.session.Session
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 userNameSessionKey = new Session.AttributeKey[String]
private val userNameSessionKey = new AttributeStore.AttributeKey[String]
def putUserName(serverSession:ServerSession, userName:String):Unit =
serverSession.setAttribute(userNameSessionKey, userName)

View File

@@ -1,7 +1,8 @@
package gitbucket.core.util
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryService, AccountService}
import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.model.Role
import RepositoryService.RepositoryInfo
import Implicits._
import ControlUtil._
@@ -40,9 +41,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true
}) => action(repository)
// TODO Repository management is allowed for only group managers?
case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository)
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Role.ADMIN)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
@@ -86,32 +87,9 @@ trait AdminAuthenticator { self: ControllerBase =>
}
/**
* Allows only collaborators and administrators.
* Allows only guests and signed in users who can access the repository.
*/
trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =>
protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
}
}
}
}
/**
* Allows only the repository owner (or manager for group repository) and administrators.
*/
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -125,7 +103,8 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
}
@@ -136,9 +115,9 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
}
/**
* Allows only signed in users which can access the repository.
* Allows only signed in users who have read permission for the repository.
*/
trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =>
trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -150,7 +129,32 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()
}
}
}
}
/**
* Allows only signed in users who have write permission for the repository.
*/
trait WritableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Role.ADMIN, Role.DEVELOPER)).contains(x.userName)) => action(repository)
case _ => Unauthorized()
}
} getOrElse NotFound()

View File

@@ -17,6 +17,11 @@ object DatabaseConfig {
| url = "jdbc:h2:${DatabaseHome};MVCC=true"
| user = "sa"
| password = "sa"
|# connectionTimeout = 30000
|# idleTimeout = 600000
|# maxLifetime = 1800000
|# minimumIdle = 10
|# maximumPoolSize = 10
|}
|""".stripMargin, "UTF-8")
}
@@ -28,12 +33,21 @@ object DatabaseConfig {
def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
lazy val url: String = url(None)
lazy val user: String = config.getString("db.user")
lazy val password: String = config.getString("db.password")
lazy val jdbcDriver: String = DatabaseType(url).jdbcDriver
lazy val slickDriver: slick.driver.JdbcProfile = DatabaseType(url).slickDriver
lazy val liquiDriver: AbstractJdbcDatabase = DatabaseType(url).liquiDriver
lazy val url : String = url(None)
lazy val user : String = config.getString("db.user")
lazy val password : String = config.getString("db.password")
lazy val jdbcDriver : String = DatabaseType(url).jdbcDriver
lazy val slickDriver : slick.driver.JdbcProfile = DatabaseType(url).slickDriver
lazy val liquiDriver : AbstractJdbcDatabase = DatabaseType(url).liquiDriver
lazy val connectionTimeout : Option[Long] = getOptionValue("db.connectionTimeout", config.getLong)
lazy val idleTimeout : Option[Long] = getOptionValue("db.idleTimeout" , config.getLong)
lazy val maxLifetime : Option[Long] = getOptionValue("db.maxLifetime" , config.getLong)
lazy val minimumIdle : Option[Int] = getOptionValue("db.minimumIdle" , config.getInt)
lazy val maximumPoolSize : Option[Int] = getOptionValue("db.maximumPoolSize" , config.getInt)
private def getOptionValue[T](path: String, f: String => T): Option[T] = {
if(config.hasPath(path)) Some(f(path)) else None
}
}

View File

@@ -85,12 +85,6 @@ object Implicits {
}
implicit class RichSession(session: HttpSession){
def putAndGet[T](key: String, value: T): T = {
session.setAttribute(key, value)
value
}
def getAndRemove[T](key: String): Option[T] = {
val value = session.getAttribute(key).asInstanceOf[T]
if(value == null){

View File

@@ -3,16 +3,16 @@ package gitbucket.core.util
import java.io._
import java.sql._
import java.text.SimpleDateFormat
import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory}
import ControlUtil._
import scala.StringBuilder
import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
/**
* Provides implicit class which extends java.sql.Connection.
* This is used in automatic migration in [[servlet.AutoUpdateListener]].
* This is used in following points:
*
* - Automatic migration in [[gitbucket.core.servlet.InitializeListener]]
* - Data importing / exporting in [[gitbucket.core.controller.SystemSettingsController]] and [[gitbucket.core.controller.FileUploadController]]
*/
object JDBCUtil {
@@ -64,65 +64,38 @@ object JDBCUtil {
}
}
def importAsXML(in: InputStream): Unit = {
def importAsSQL(in: InputStream): Unit = {
conn.setAutoCommit(false)
try {
val factory = XMLInputFactory.newInstance()
using(factory.createXMLStreamReader(in, "UTF-8")){ reader =>
// stateful objects
var elementName = ""
var insertTable = ""
var insertColumns = Map.empty[String, (String, String)]
using(in){ in =>
var out = new ByteArrayOutputStream()
while(reader.hasNext){
reader.next()
var length = 0
val bytes = new scala.Array[Byte](1024 * 8)
var stringLiteral = false
reader.getEventType match {
case XMLStreamConstants.START_ELEMENT =>
elementName = reader.getName.getLocalPart
if(elementName == "insert"){
insertTable = reader.getAttributeValue(null, "table")
} else if(elementName == "delete"){
val tableName = reader.getAttributeValue(null, "table")
conn.update(s"DELETE FROM ${tableName}")
} else if(elementName == "column"){
val columnName = reader.getAttributeValue(null, "name")
val columnType = reader.getAttributeValue(null, "type")
val columnValue = reader.getElementText
insertColumns = insertColumns + (columnName -> (columnType, columnValue))
}
case XMLStreamConstants.END_ELEMENT =>
// Execute insert statement
reader.getName.getLocalPart match {
case "insert" => {
val sb = new StringBuilder()
sb.append(s"INSERT INTO ${insertTable} (")
sb.append(insertColumns.map { case (columnName, _) => columnName }.mkString(", "))
sb.append(") VALUES (")
sb.append(insertColumns.map { case (_, (columnType, columnValue)) =>
if(columnType == null || columnValue == null){
"NULL"
} else if(columnType == "string"){
"'" + columnValue.replace("'", "''") + "'"
} else if(columnType == "timestamp"){
"'" + columnValue + "'"
} else {
columnValue.toString
}
}.mkString(", "))
sb.append(")")
conn.update(sb.toString)
insertColumns = Map.empty[String, (String, String)] // Clear column information
}
case _ => // Nothing to do
}
case _ => // Nothing to do
while({ length = in.read(bytes); length != -1 }){
for(i <- 0 to length - 1){
val c = bytes(i)
if(c == '\''){
stringLiteral = !stringLiteral
}
if(c == ';' && !stringLiteral){
val sql = new String(out.toByteArray, "UTF-8")
conn.update(sql.trim)
out = new ByteArrayOutputStream()
} else {
out.write(c)
}
}
}
}
val remain = out.toByteArray
if(remain.length != 0){
val sql = new String(remain, "UTF-8")
conn.update(sql.trim)
}
}
conn.commit()
} catch {
@@ -133,68 +106,6 @@ object JDBCUtil {
}
}
def exportAsXML(targetTables: Seq[String]): File = {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val file = File.createTempFile("gitbucket-export-", ".xml")
val factory = XMLOutputFactory.newInstance()
using(factory.createXMLStreamWriter(new FileOutputStream(file), "UTF-8")){ writer =>
val dbMeta = conn.getMetaData
val allTablesInDatabase = allTablesOrderByDependencies(dbMeta)
writer.writeStartDocument("UTF-8", "1.0")
writer.writeStartElement("tables")
println(allTablesInDatabase.mkString(", "))
allTablesInDatabase.reverse.foreach { tableName =>
if (targetTables.contains(tableName)) {
writer.writeStartElement("delete")
writer.writeAttribute("table", tableName)
writer.writeEndElement()
}
}
allTablesInDatabase.foreach { tableName =>
if (targetTables.contains(tableName)) {
select(s"SELECT * FROM ${tableName}") { rs =>
writer.writeStartElement("insert")
writer.writeAttribute("table", tableName)
val rsMeta = rs.getMetaData
(1 to rsMeta.getColumnCount).foreach { i =>
val columnName = rsMeta.getColumnName(i)
val (columnType, columnValue) = if(rs.getObject(columnName) == null){
(null, null)
} else {
rsMeta.getColumnType(i) match {
case Types.BOOLEAN | Types.BIT => ("boolean", rs.getBoolean(columnName))
case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => ("string", rs.getString(columnName))
case Types.INTEGER => ("int", rs.getInt(columnName))
case Types.TIMESTAMP => ("timestamp", dateFormat.format(rs.getTimestamp(columnName)))
}
}
writer.writeStartElement("column")
writer.writeAttribute("name", columnName)
if(columnType != null){
writer.writeAttribute("type", columnType)
}
if(columnValue != null){
writer.writeCharacters(columnValue.toString)
}
writer.writeEndElement()
}
writer.writeEndElement()
}
}
}
writer.writeEndElement()
writer.writeEndDocument()
}
file
}
def exportAsSQL(targetTables: Seq[String]): File = {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val file = File.createTempFile("gitbucket-export-", ".sql")

View File

@@ -830,14 +830,16 @@ object JGitUtil {
existIds.toSeq
}
def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = {
def processTree[T](git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => T): Seq[T] = {
using(new RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true)
val result = new collection.mutable.ListBuffer[T]()
while(treeWalk.next){
f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
result += f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
}
result.toSeq
}
}
}

View File

@@ -22,8 +22,10 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
(
// individual repository's owner
issue.userName ::
// group members of group repository
getGroupMembers(issue.userName).map(_.userName) :::
// collaborators
getCollaborators(issue.userName, issue.repositoryName) :::
getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
// participants
issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)

View File

@@ -86,8 +86,9 @@ object StringUtil {
*@param message the message which may contains issue id
* @return the iterator of issue id
*/
def extractIssueId(message: String): Iterator[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
def extractIssueId(message: String): Seq[String] =
"(^|\\W)#(\\d+)(\\W|$)".r
.findAllIn(message).matchData.map(_.group(2)).toSeq.distinct
/**
* Extract close issue id like ```close #issueId ``` from the given message.
@@ -95,7 +96,8 @@ object StringUtil {
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
def extractCloseId(message: String): Seq[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
.findAllIn(message).matchData.map(_.group(1)).toSeq.distinct
}

View File

@@ -37,7 +37,7 @@ trait LinkConverter { self: RequestCache =>
// convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a>"""
s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a></code>"""
}
}
@@ -56,7 +56,7 @@ trait LinkConverter { self: RequestCache =>
// convert username@SHA to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a></code>"""
}
}
@@ -93,6 +93,8 @@ trait LinkConverter { self: RequestCache =>
}
// convert commit id to link
.replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
.replaceBy("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))".r){ m =>
Some(s"""<code><a href="${context.path}/${repository.owner}/${repository.name}/commit/${m.group(2)}">${m.group(2).substring(0, 7)}</a></code>""")
}
}
}

View File

@@ -147,21 +147,23 @@ object Markdown {
}
private def fixUrl(url: String, isImage: Boolean = false): String = {
lazy val urlWithRawParam: String = url + (if(isImage && !url.endsWith("?raw=true")) "?raw=true" else "")
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
url
} else if(url.startsWith("#")){
("#" + generateAnchorName(url.substring(1)))
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
urlWithRawParam
} else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam
} else {
val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam
}
} else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url

View File

@@ -31,7 +31,7 @@
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200)
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
@@ -80,15 +80,14 @@ $(function(){
}
// check existence
$.post('@context.path/_user/existence', {
'userName': userName
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data == 'user'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){

View File

@@ -1,13 +1,14 @@
@(account: gitbucket.core.model.Account, members: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
@members.map { member =>
<div class="block">
<div class="block-header">
@helpers.avatar(userName, 20) <a href="@helpers.url(userName)">@userName</a>
@helpers.avatar(member.userName, 20) <a href="@helpers.url(member.userName)">@member.userName</a>
@if(member.isManager){ (Manager) }
</div>
</div>
}

View File

@@ -13,22 +13,12 @@
</div>
}
<input type="submit" class="btn btn-success pull-right" value="Export">
<div class="radio pull-right" style="margin-right: 10px;">
<label>
<input type="radio" name="type" value="sql">SQL
</label>
</div>
<div class="radio pull-right" style="margin-right: 10px;">
<label>
<input type="radio" name="type" value="xml" checked>XML
</label>
</div>
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Import (only XML)</div>
<div class="panel-heading strong">Import</div>
<div class="panel-body">
<form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
<input type="file" name="file" id="file">
@@ -42,10 +32,10 @@
$(function(){
$('#import-form').submit(function(){
if($('#file').val() == ''){
alert('Choose an import XML file.');
alert('Choose an import SQL file.');
return false;
} else if(!$('#file').val().endsWith(".xml")){
alert('Import is available for only the XML file.');
} else if(!$('#file').val().endsWith(".sql")){
alert('Import is available for only the SQL file.');
return false;
}
return confirm('All existing data is deleted before importing.\nAre you sure?');

View File

@@ -1,16 +1,16 @@
@(plugins: List[(gitbucket.core.plugin.PluginInfo, String)])(implicit context: gitbucket.core.controller.Context)
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Plugins"){
@gitbucket.core.admin.html.menu("plugins") {
<h1>Installed plugins</h1>
@if(plugins.size > 0) {
<ul>
@plugins.map { case (plugin, migrationVersion) =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@migrationVersion</a></li>
@plugins.map { plugin =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
}
</ul>
@plugins.map { case (plugin, migrationVersion) =>
@plugins.map { plugin =>
<div class="panel panel-default">
<div class="panel-heading strong">@plugin.pluginName</div>
<div class="panel-body">
@@ -20,7 +20,7 @@
</div>
<div class="row">
<label class="col-md-2">Version</label>
<span class="col-md-10">@migrationVersion @if(plugin.pluginVersion != migrationVersion){ <span class="error">(Migration is failed, installed version is @plugin.pluginVersion)</span> }</span>
<span class="col-md-10">@plugin.pluginVersion</span>
</div>
<div class="row">
<label class="col-md-2">Name</label>

View File

@@ -34,7 +34,7 @@
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200)
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
@@ -75,16 +75,14 @@ $(function(){
}
// check existence
$.post('@context.path/_user/existence', {
'userName': userName,
'userOnly': true
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data == 'user'){
addMemberHTML(userName, false);
} else {
$('#error-members').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){

View File

@@ -11,7 +11,7 @@
@gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@gitbucket.core.dashboard.html.tab("issues")
<div class="container">
@gitbucket.core.dashboard.html.issuesnavi(filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
}

View File

@@ -1,4 +1,5 @@
@(filter: String,
@(active: String,
filter: String,
openCount: Int,
closedCount: Int,
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
@@ -9,15 +10,18 @@
<li class="@(if(condition.state == "closed"){"active"})">
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
</li>
@*
<li class="@if(filter == "created_by"){active}">
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
</li>
<li class="@if(filter == "assigned"){active}">
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
</li>
<li class="@if(filter == "mentioned"){active}">
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</li>
*@
</ul>
<div class="btn-group pull-right" data-toggle="buttons">
<a class="switch btn btn-default @if(filter == "created_by"){active}" href="@context.path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
<a class="switch btn btn-default @if(filter == "assigned" ){active}" href="@context.path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
<a class="switch btn btn-default @if(filter == "mentioned" ){active}" href="@context.path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</div>
<script>
$(function(){
$('a.switch').click(function(){
location.href = $(this).attr('href');
});
})
</script>

View File

@@ -11,7 +11,7 @@
@gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@gitbucket.core.dashboard.html.tab("pulls")
<div class="container">
@gitbucket.core.dashboard.html.issuesnavi(filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
@gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
}

View File

@@ -0,0 +1,7 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
</head>
</html>

View File

@@ -1,12 +1,19 @@
@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context)
@(id: String, width: Int, user: Boolean, group: Boolean)(implicit context: gitbucket.core.controller.Context)
<span style="margin-right: 0px;">
<input type="text" name="@id" id="@id" class="form-control" autocomplete="off" style="width: @{width}px; margin-bottom: 0px; display: inline; vertical-align: middle;"/>
</span>
<script>
$(function(){
$('#@id').typeahead({
// highlighter: function(item) {
// var x = item.split(':');
// return $('<div><strong>' + x[0] + '</strong>' + (x[1] == 'true' ? ' (group)' : '') + '</div>');
// },
// updater: function (item) {
// return item.split(':')[0];
// },
source: function (query, process) {
return $.get('@context.path/_user/proposals', { query: query },
return $.get('@context.path/_user/proposals', { query: query, user: @user, group: @group },
function (data) {
return process(data.options);
});

View File

@@ -46,18 +46,11 @@ $(function(){
@if(generateScript){
try {
$([$('#@textareaId')[0]]).dropzone({
@dropzone(false, textareaId)
});
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: 10,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
'](@context.baseUrl/@repository.owner/@repository.name/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + attachFile);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
@dropzone(true, textareaId)
});
} catch(e) {
if (e.message !== "Dropzone already attached.") {
@@ -68,3 +61,17 @@ $(function(){
});
</script>
}
@dropzone(clickable: Boolean, textareaId: Option[String]) = {
url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: 10,
clickable: false,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +
'](@context.baseUrl/@repository.owner/@repository.name/_attached/' + id + ')';
$('#@textareaId').val($('#@textareaId').val() + attachFile);
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
}
}

View File

@@ -7,7 +7,7 @@
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree"
) {
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li>
<li><input id="branch-control-input" type="text" class="form-control input-sm" placeholder="Find or create branch ..."/></li>
<li><input id="branch-control-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Find or create branch ..."/></li>
@body
@if(hasWritePermission) {
<li id="create-branch" style="display: none;">

View File

@@ -8,9 +8,9 @@
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
<div class="issue-avatar-image">@helpers.avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default commit-comment-box commit-comment-@comment.commentId">
<div class="panel-heading">
@helpers.avatar(comment.commentedUserName, 20)
@helpers.user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
commented

View File

@@ -1,7 +1,8 @@
@(value : String = "",
prefix: String = "",
style : String = "",
right : Boolean = false)(body: Html)
right : Boolean = false,
filter: String = "")(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
@@ -16,6 +17,28 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu@if(right){ pull-right}">
@if(filter.nonEmpty) {
<li><input id="@filter-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Filter"/></li>
}
@body
</ul>
</div>
@if(filter.nonEmpty) {
<script>
$(function(){
$('#@{filter}-input').parent().click(function(e) {
e.stopPropagation();
});
$('#@{filter}-input').keyup(function() {
var inputVal = $('#@{filter}-input').val();
$.each($('#@{filter}-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();
}
});
});
});
</script>
}

View File

@@ -30,7 +30,7 @@
@gitbucket.core.helper.html.attached(
repository = repository,
completionContext = completionContext,
generateScript = enableWikiLink
generateScript = !enableWikiLink
)(textarea)
</div>
<div class="tab-pane" id="tab@(uid+1)">

View File

@@ -1,12 +1,12 @@
@(issue: gitbucket.core.model.Issue,
reopenable: Boolean,
hasWritePermission: Boolean,
isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@if(context.loginAccount.isDefined){
@if(isEditable){
<hr/><br/>
<form method="POST" validate="true">
<div class="issue-avatar-image">@helpers.avatarLink(context.loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-comment-box">
<div class="panel-body">
@gitbucket.core.helper.html.preview(
@@ -16,7 +16,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
hasWritePermission = isEditable,
completionContext = "issues",
style = "",
elastic = true,
@@ -24,7 +24,7 @@
)
<div class="text-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == context.loginAccount.get.userName)){
@if((reopenable || !issue.closed) && (isManageable || issue.openedUserName == context.loginAccount.get.userName)){
<input type="submit" class="btn btn-default" tabindex="3" formaction="@helpers.url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
<input type="submit" class="btn btn-success" tabindex="2" formaction="@helpers.url(repository)/issue_comments/new" value="Comment"/>

View File

@@ -1,17 +1,18 @@
@(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment],
hasWritePermission: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.model.CommitComment
@if(issue.isDefined){
<div class="issue-avatar-image">@helpers.avatarLink(issue.get.openedUserName, 48)</div>
<div class="panel panel-default issue-comment-box">
<div class="panel-heading">
@helpers.user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate)</span>
@helpers.avatar(issue.get.openedUserName, 20)
@helpers.user(issue.get.openedUserName, styleClass="username strong")
<span class="muted">commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
}
</span>
@@ -24,7 +25,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
hasWritePermission = isManageable
)
</div>
</div>
@@ -35,9 +36,9 @@
case comment: gitbucket.core.model.IssueComment => {
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
&& comment.action != "commit" && comment.action != "refer"){
<div class="issue-avatar-image">@helpers.avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading">
@helpers.avatar(comment.commentedUserName, 20)
@helpers.user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
@@ -48,7 +49,7 @@
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</span>
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
&& (hasWritePermission || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
&& (isManageable || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<span class="pull-right">
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
@@ -63,7 +64,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
hasWritePermission = isManageable
)
</div>
</div>
@@ -166,7 +167,7 @@
}
}
case comment: CommitComment => {
@gitbucket.core.helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo))
@gitbucket.core.helper.html.commitcomment(comment, isManageable, repository, pullreq.map(_.commitIdTo))
}
}
<script>

View File

@@ -1,7 +1,7 @@
@(collaborators: List[String],
milestones: List[gitbucket.core.model.Milestone],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@@ -18,7 +18,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
hasWritePermission = isManageable,
completionContext = "issues",
style = "height: 200px; max-height: 250px;",
elastic = true
@@ -28,7 +28,7 @@
</div>
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, isManageable, repository)
</div>
</div>
</form>

View File

@@ -4,17 +4,20 @@
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("issues", repository){
<div>
<div class="show-title pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-default" href="#" id="edit">Edit</a>
}
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
@if(isEditable){
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
}
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
@@ -47,11 +50,11 @@
<hr>
<div style="margin-top: 15px;">
<div class="col-md-9">
@gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository)
@gitbucket.core.issues.html.commentform(issue, true, hasWritePermission, repository)
@gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository)
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository)
</div>
</div>
}

View File

@@ -4,14 +4,14 @@
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
<div style="margin-bottom: 14px;">
<span class="muted small strong">Labels</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "labels") {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@@ -34,9 +34,9 @@
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "milestone") {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
@@ -74,7 +74,7 @@
<span id="label-milestone">
@issue.flatMap(_.milestoneId).map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
<a class="strong small username" href="@helpers.url(repository)/issues?milestone=@helpers.urlEncode(milestone.title)&state=open">@milestone.title</a>
}
}.getOrElse {
<span class="muted small">No milestone</span>
@@ -86,9 +86,9 @@
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, 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>
@@ -210,7 +210,8 @@ $(function(){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
$('#label-milestone').html($('<a class="strong small username">').text(title)
.attr('href', '@helpers.url(repository)/issues?milestone=' + encodeURIComponent(title) + '&state=open'));
if(progress){
$('#milestone-progress-area').html(progress);
}

View File

@@ -8,7 +8,8 @@
closedCount: Int,
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
isEditable: Boolean,
isManageable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu(target, repository){
@@ -21,7 +22,7 @@
</li>
</ul>
<form method="GET" id="search-filter-form" class="form-inline pull-right">
@if(context.loginAccount.isDefined){
@if(isEditable){
@if(target == "issues"){
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
}
@@ -30,8 +31,8 @@
}
}
</form>
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), isManageable)
@if(isManageable){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
@@ -40,7 +41,7 @@
}
}
}
@if(hasWritePermission){
@if(isManageable){
<script>
$(function(){
$('a.header-link').mouseover(function(e){

View File

@@ -8,7 +8,7 @@
milestones: List[gitbucket.core.model.Milestone] = Nil,
labels: List[gitbucket.core.model.Label] = Nil,
repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: gitbucket.core.controller.Context)
isManageable: Boolean = false)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.service.IssuesService.IssueInfo
@*
@@ -110,7 +110,7 @@
</li>
}
</span>
@if(hasWritePermission){
@if(isManageable){
<span id="table-issues-batchedit">
@gitbucket.core.helper.html.dropdown("Mark as") {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
@@ -174,7 +174,7 @@
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(hasWritePermission){
@if(isManageable){
<input type="checkbox" value="@issue.issueId"/>
}
@*

View File

@@ -15,8 +15,9 @@
<link href="@helpers.assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@helpers.assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@helpers.assets/vendors/facebox/facebox.css" rel="stylesheet"/>
<link href="@helpers.assets/vendors/AdminLTE-2.2.3/css/AdminLTE.min.css" rel="stylesheet">
<link href="@helpers.assets/vendors/AdminLTE-2.2.3/css/skins/skin-blue.min.css" rel="stylesheet">
<link href="@helpers.assets/vendors/AdminLTE-2.3.6/css/AdminLTE.min.css" rel="stylesheet">
<link href="@helpers.assets/vendors/AdminLTE-2.3.6/css/skins/skin-blue.min.css" rel="stylesheet">
<link href="@helpers.assets/vendors/font-awesome-4.6.3/css/font-awesome.min.css" rel="stylesheet">
<link href="@helpers.assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@helpers.assets/vendors/jquery/jquery-1.11.1.js"></script>
<script src="@helpers.assets/vendors/dropzone/dropzone.js"></script>
@@ -34,13 +35,11 @@
<script src="@helpers.assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
<script src="@helpers.assets/vendors/jquery-textcomplete-1.6.2/jquery.textcomplete.js"></script>
@repository.map { repository =>
@if(!repository.repository.isPrivate){
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
}
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
}
<script src="@helpers.assets/vendors/AdminLTE-2.2.3/js/app.js" type="text/javascript"></script>
<script src="@helpers.assets/vendors/AdminLTE-2.3.6/js/app.js" type="text/javascript"></script>
</head>
<body class="skin-blue">
<body class="skin-blue page-load @if(context.sidebarCollapse){sidebar-collapse}">
<div class="wrapper">
<header class="main-header">
<a href="@context.path/" class="logo">
@@ -49,8 +48,14 @@
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a>
<nav class="navbar navbar-static-top" role="navigation">
<!-- Sidebar toggle button-->
@if(body.toString.contains("main-sidebar")){
<a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button">
<span class="sr-only">Toggle navigation</span>
</a>
}
@repository.map { repository =>
<form id="search" action="@context.path/search" method="POST" class="navbar-form navbar-left" role="search">
<form id="search" action="@context.path/search" method="POST" 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="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
@@ -58,7 +63,7 @@
</div>
</form>
}
<ul class="nav navbar-nav">
<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>
@@ -110,6 +115,9 @@
$('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != '';
});
$(".sidebar-toggle").on('click', function(e){
$.get('@context.path/sidebar-collapse', { collapse: !$('body').hasClass('sidebar-collapse') });
});
});
</script>
@PluginRegistry().getJavaScript(context.request.getRequestURI).map { script =>

View File

@@ -9,11 +9,11 @@
<li @if(active == name){class="active"}>
@if(path.startsWith("http")){
<a href="@path" target="_blank">
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
</a>
} else {
<a href="@helpers.url(repository)@path">
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
</a>
}
</li>
@@ -27,24 +27,26 @@
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
}
@if(repository.repository.enableIssues) {
@if(repository.repository.options.issuesOption != "DISABLE") {
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls", "pulls", "Pull Requests", "git-pull-request", repository.pullCount)
@menuitem("/issues/labels", "labels", "Labels", "tag")
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
} else {
@repository.repository.externalIssuesUrl.map { externalIssuesUrl =>
@repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
@menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened")
}
}
@if(repository.repository.enableWiki) {
@if(repository.repository.options.wikiOption != "DISABLE") {
@menuitem("/wiki", "wiki", "Wiki", "book")
} else {
@repository.repository.externalWikiUrl.map { externalWikiUrl =>
@repository.repository.options.externalWikiUrl.map { externalWikiUrl =>
@menuitem(externalWikiUrl, "wiki", "Wiki", "book")
}
}
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
@if(repository.repository.options.allowFork) {
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
}
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
@menuitem("/settings", "settings", "Settings", "tools")
}

View File

@@ -51,7 +51,7 @@
&nbsp;&nbsp;
<span class="muted">Discuss and review the changes in this comparison with others.</span>
</div>
<div id="pull-request-form" @*class="box"*@ style="display: none; margin-bottom: 20px;">
<div id="pull-request-form" style="display: none; margin-bottom: 20px;">
<form method="POST" action="@context.path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div class="row">
<div class="col-md-9">
@@ -130,7 +130,7 @@
</tr>
</table>
</div>
<div class="box" style="margin-bottom: 20px;">
<div style="margin-bottom: 20px;">
@commits.map { day =>
<div style="margin-top: 8px; margin-bottom: 8px;" class="muted">
Commits on @helpers.date(day.head.commitTime)

View File

@@ -5,12 +5,13 @@
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
<div class="col-md-9">
<div id="comment-list">
@gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
@gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository, Some(pullreq))
</div>
@defining(comments.flatMap {
case comment: gitbucket.core.model.IssueComment => Some(comment)
@@ -25,7 +26,7 @@
</div>
</div>
}
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&
@if(isManageable && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
@@ -37,11 +38,11 @@
</div>
</div>
}
@gitbucket.core.issues.html.commentform(issue, !merged, hasWritePermission, repository)
@gitbucket.core.issues.html.commentform(issue, !merged, isEditable, isManageable, repository)
}
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository)
</div>
<script>
$(function(){
@@ -55,7 +56,7 @@ $(function(){
$.get('@helpers.url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
}
@if(hasWritePermission){
@if(isManageable){
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');

View File

@@ -7,7 +7,8 @@
labels: List[gitbucket.core.model.Label],
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
hasWritePermission: Boolean,
isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
flash: Map[String, String])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -18,7 +19,7 @@
@defining(dayByDayCommits.flatten){ commits =>
<div>
<div class="show-title pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a>
}
@if(context.loginAccount.isDefined){
@@ -82,13 +83,13 @@
@flash.get("info").map{ info =>
<div class="alert alert-info">@info</div>
}
@gitbucket.core.pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@gitbucket.core.pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, isEditable, isManageable, repository)
</div>
<div class="tab-pane" id="commits">
@gitbucket.core.pulls.html.commits(dayByDayCommits, Some(comments), repository)
</div>
<div class="tab-pane" id="files">
@gitbucket.core.helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true)
@gitbucket.core.helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), isManageable, true)
</div>
</div>
}

View File

@@ -73,7 +73,7 @@
</div>
} else {
<div class="box-content-bottom">
<pre class="prettyprint linenums blob @if(!isRenderable){ no-renderable } ">@content.content.get</pre>
<pre class="prettyprint linenums blob @if(!isRenderable){ no-renderable } ">@content.content.map(_.replaceAll("^(\r?\n)", "$1$1"))</pre>
</div>
}
}
@@ -150,7 +150,7 @@ $(window).load(function(){
var mode = m[1];
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1] == 'blame' ? '/blob' : '/blame') + m[2]);
if(pre.parents("div.box-content-bottom").find(".blame").length){
pre.parents("div.container").toggleClass("blame-container", mode == 'blame');
pre.parent().toggleClass("blame-container", mode == 'blame');
updateSourceLineNum();
return;
}
@@ -161,7 +161,7 @@ $(window).load(function(){
$(document.body).toggleClass('no-box-shadow', document.body.style.boxShadow===undefined);
$('.blame-action').addClass("active");
var base = $('<div class="blame">').css({height: pre.height()}).prependTo(pre.parents("div.box-content-bottom"));
base.parents("div.container").addClass("blame-container");
base.parent().addClass("blame-container");
updateSourceLineNum();
$.get($('.blame-action').data('url')).done(function(data){
var blame = data.blame;

View File

@@ -4,81 +4,81 @@
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@gitbucket.core.html.menu("branches", repository){
<table class="table table-bordered table-hover branches">
<table class="table table-hover branches">
<thead>
<tr>
<th style="background: #f5f5f5;color: #666;">All branches</th>
<th style="background: #f5f5f5;color: #666;" colspan="3">All branches</th>
</tr>
</thead>
<tbody>
@branchInfo.map { case (branch, prs, isProtected) =>
<tr>
<td style="padding: 12px;">
<div class="branch-action">
@branch.mergeInfo.map{ info =>
@prs.map{ case (pull, issue) =>
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title">#@issue.issueId</a>
@if(issue.closed) {
@if(info.isMerged){
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-info">Merged</a>
} else {
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-important">Closed</a>
}
} else {
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-success">Open</a>
}
}.getOrElse{
@if(context.loginAccount.isDefined){
<a href="@helpers.url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
helpers.urlEncode(parent) + ":" + helpers.encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
helpers.encodeRefName(repository.repository.defaultBranch)
}}...@{helpers.encodeRefName(branch.name)}?expand=1" class="btn btn-success">New Pull Request</a>
} else {
<a href="@helpers.url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
helpers.urlEncode(parent) + ":" + helpers.encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
helpers.encodeRefName(repository.repository.defaultBranch)
}}...@{helpers.encodeRefName(branch.name)}" class="btn btn-success">Compare</a>
}
}
@if(hasWritePermission){
<span style="margin-left: 8px;">
@if(prs.map(!_._2.closed).getOrElse(false)){
<a class="disabled" data-toggle="tooltip" title="You cant delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
} else {
@if(isProtected){
<a class="disabled" data-toggle="tooltip" title="You cant delete a protected branch."><i class="octicon octicon-trashcan"></i></a>
} else {
<a href="@helpers.url(repository)/delete/@helpers.encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
}
}
</span>
}
}
</div>
<div class="branch-details">
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)" class="branch-name">@branch.name</a>
<span class="branch-meta">
<span>Updated @gitbucket.core.helper.html.datetimeago(branch.commitTime, false)
by <span>@helpers.user(branch.committerName, branch.committerEmailAddress, "muted-link")</span>
</span>
<td class="branch-details">
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)" class="branch-name">@branch.name</a>
<span class="branch-meta">
<span>Updated @gitbucket.core.helper.html.datetimeago(branch.commitTime, false)
by <span>@helpers.user(branch.committerName, branch.committerEmailAddress, "muted-link")</span>
</span>
</div>
<div class="branch-a-b-count">
@if(branch.mergeInfo.isEmpty){
<span class="badge">Default</span>
} else {
@branch.mergeInfo.map{ info =>
<div data-toggle="tooltip" title="@info.ahead commits ahead, @info.behind commits behind @repository.repository.defaultBranch">
<div class="a-b-count-widget">
<div class="count-half"><div class="count-value">@info.ahead</div></div>
<div class="count-half"><div class="count-value">@info.behind</div></div>
</div>
</span>
</td>
<td class="branch-a-b-count">
@if(branch.mergeInfo.isEmpty){
<span class="badge">Default</span>
} else {
@branch.mergeInfo.map{ info =>
<div data-toggle="tooltip" title="@info.ahead commits ahead, @info.behind commits behind @repository.repository.defaultBranch">
<div class="a-b-count-widget">
<div class="count-half"><div class="count-value">@info.ahead</div></div>
<div class="count-half"><div class="count-value">@info.behind</div></div>
</div>
</div>
}
}
</td>
<td>
<div class="branch-action">
@branch.mergeInfo.map{ info =>
@prs.map{ case (pull, issue) =>
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title">#@issue.issueId</a>
@if(issue.closed) {
@if(info.isMerged){
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-info">Merged</a>
} else {
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-important">Closed</a>
}
} else {
<a href="@helpers.url(repository)/pull/@issue.issueId" title="@issue.title" class="label label-success">Open</a>
}
}.getOrElse{
@if(context.loginAccount.isDefined){
<a href="@helpers.url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
helpers.urlEncode(parent) + ":" + helpers.encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
helpers.encodeRefName(repository.repository.defaultBranch)
}}...@{helpers.encodeRefName(branch.name)}?expand=1" class="btn btn-default">New Pull Request</a>
} else {
<a href="@helpers.url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
helpers.urlEncode(parent) + ":" + helpers.encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
helpers.encodeRefName(repository.repository.defaultBranch)
}}...@{helpers.encodeRefName(branch.name)}" class="btn btn-default">Compare</a>
}
}
@if(hasWritePermission){
<span style="margin-left: 8px;">
@if(prs.map(!_._2.closed).getOrElse(false)){
<a class="disabled" data-toggle="tooltip" title="You cant delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
} else {
@if(isProtected){
<a class="disabled" data-toggle="tooltip" title="You cant delete a protected branch."><i class="octicon octicon-trashcan"></i></a>
} else {
<a href="@helpers.url(repository)/delete/@helpers.encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
}
}
</span>
}
}
</div>
</td>
</tr>

View File

@@ -33,7 +33,7 @@
}
</div>
<table class="table table-bordered">
<table class="table table-bordered table-hover">
@commits.map { day =>
<tr>
<th rowspan="@day.size" width="100">@helpers.date(day.head.commitTime)</th>

View File

@@ -22,21 +22,21 @@
}, Some(repository)) {
@gitbucket.core.html.menu("files", repository, Some(branch), info, error){
<div class="head">
<div class="pull-right pc">
<div class="pull-right">
<div class="btn-group">
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default pc" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i></a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(repository.commitCount > 10000){10000+} else {@repository.commitCount} @helpers.plural(repository.commitCount, "commit")</a>
</div>
</div>
@if(pathList.isEmpty){
@if(context.platform != "linux" && context.platform != null){
<div class="pull-right pc" style="margin-right: 5px;">
<div class="btn-group">
<div class="pull-right pc" style="margin-right: 5px;">
<div class="btn-group">
@if(context.platform != "linux" && context.platform != null){
<a href="@RepositoryService.openRepoUrl(repository.httpUrl)" id="repository-clone-url" class="btn btn-sm btn-default"><i class="octicon octicon-desktop-download"></i></a>
<a href="@{helpers.url(repository)}/archive/@{helpers.encodeRefName(branch)}.zip" class="btn btn-sm btn-default"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
</div>
}
<a href="@{helpers.url(repository)}/archive/@{helpers.encodeRefName(branch)}.zip" class="btn btn-sm btn-default"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
</div>
}
</div>
<div class="pull-right pc">
<div style="width: 240px; margin-right: 5px; margin-left: 5px;">
@gitbucket.core.helper.html.copy("repository-url-copy", repository.httpUrl){

View File

@@ -1,34 +1,140 @@
@(collaborators: List[String],
@(collaborators: List[(gitbucket.core.model.Collaborator, Boolean)],
isGroupRepository: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.model.Role
@gitbucket.core.html.main("Settings", Some(repository)){
@gitbucket.core.html.menu("settings", repository){
@gitbucket.core.settings.html.menu("collaborators", repository){
<h3>Manage Collaborators</h3>
<ul class="collaborator">
@collaborators.map { collaboratorName =>
<li>
<a href="@helpers.url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){
<a href="@helpers.url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
}
</li>
}
</ul>
@if(!isGroupRepository){
<form method="POST" action="@helpers.url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div>
<span class="error" id="error-userName"></span>
<form id="form" method="post" action="@helpers.url(repository)/settings/collaborators">
<div class="panel panel-default">
<div class="panel-heading strong">Collaborators</div>
<div class="panel-body">
<ul id="collaborator-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("userName-collaborator", 200, true, false)
<input type="button" class="btn btn-default add" value="Add" id="addCollaborator"/>
<div>
<span class="error" id="error-collaborator"></span>
</div>
</div>
@gitbucket.core.helper.html.account("userName", 300)
<input type="submit" class="btn btn-default" value="Add"/>
</form>
}
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Groups</div>
<div class="panel-body">
<ul id="group-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("userName-group", 200, false, true)
<input type="button" class="btn btn-default add" value="Add" id="addGroup"/>
<div>
<span class="error" id="error-group"></span>
</div>
</div>
</div>
<div class="align-right" style="margin-top: 20px;">
<input type="hidden" id="collaborators" name="collaborators" />
<input type="submit" class="btn btn-success" value="Apply changes"/>
</div>
</form>
}
}
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateValues();
});
$('.add').click(function(){
var id = $(this).attr('id') == 'addCollaborator' ? 'collaborator' : 'group';
$('#error-' + id).text('');
var userName = $('#userName-' + id).val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check owner
var owner = '@repository.owner' == userName
if(owner){
$('#error-' + id).text('User is owner of this repository.');
return false;
}
// check duplication
var exists = $('#' + id + '-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-' + id).text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data != ''){
addListHTML(userName, '@Role.ADMIN.name', '#' + id + '-list');
$('#userName-' + id).val('');
} else {
$('#error-' + id).text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#userName-collaborator, #userName-group').keypress(function(e){
return !(e.keyCode == 13);
});
@collaborators.map { case (collaborator, isGroup) =>
addListHTML('@collaborator.collaboratorName', '@collaborator.role', @if(isGroup){'#group-list'}else{'#collaborator-list'});
}
function addListHTML(userName, role, id){
var adminButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.ADMIN.name" name="' + userName + '">Admin</label>');
if(role == '@Role.ADMIN.name'){
adminButton.addClass('active');
}
var writeButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.DEVELOPER.name" name="' + userName + '">Developer</label>');
if(role == '@Role.DEVELOPER.name'){
writeButton.addClass('active');
}
var readButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.GUEST.name" name="' + userName + '">Guest</label>');
if(role == '@Role.GUEST.name'){
readButton.addClass('active');
}
$(id).append($('<li>')
.data('name', userName)
.append($('<div class="btn-group role" data-toggle="buttons">')
.append(adminButton)
.append(writeButton)
.append(readButton))
.append(' ')
.append($('<a target="_blank">').attr('href', '@context.path/' + userName).text(userName))
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateValues(){
var collaborators = $('#collaborator-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
var groups = $('#group-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
$('#collaborators').val(collaborators + ',' + groups);
}
});
</script>

View File

@@ -13,7 +13,7 @@
<div>
Transfer this repo to another user or to group.
<div class="pull-right">
@gitbucket.core.helper.html.account("newOwner", 200)
@gitbucket.core.helper.html.account("newOwner", 200, true, true)
<input type="submit" class="btn btn-danger" value="Transfer"/>
<div>
<span id="error-newOwner" class="error"></span>

View File

@@ -39,40 +39,76 @@
</div>
</label>
</fieldset>
<fieldset class="form-group">
<label class="checkbox" for="allowFork">
<input type="checkbox" id="allowFork" name="allowFork"@if(repository.repository.options.allowFork){ checked}/>
Forks<br>
<div class="normal muted">
Allow repository forking to users who can access this repository.
</div>
</label>
</fieldset>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Features</div>
<div class="panel-heading strong">Issues</div>
<div class="panel-body">
<fieldset class="form-group">
<label class="checkbox" for="enableIssues">
<input type="checkbox" id="enableIssues" name="enableIssues"@if(repository.repository.enableIssues){ checked}/>
Issues<br>
<div class="normal muted">
Provides Lightweight issue tracking integrated with this repository. Add issues to milestones, label issues, and close & reference issues from commit messages.
</div>
</label>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="DISABLE" @if(repository.repository.options.issuesOption == "DISABLE"){ checked}> Disables issues tracking system
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="PRIVATE" @if(repository.repository.options.issuesOption == "PRIVATE"){ checked}> Developers can view, create and comment on issues
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="PUBLIC" @if(repository.repository.options.issuesOption == "PUBLIC"){ checked}> Developers and guests can view, create and comment on isues
</label>
</div>
<div class="radio for-public-repo">
<label>
<input type="radio" name="issuesOption" value="ALL" @if(repository.repository.options.issuesOption == "ALL"){ checked}> All users can view, create and comment on isues
</label>
</div>
<label for="externalIssuesUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external issue tracking system for this project)</span>
</label>
<input type="text" class="form-control" id="externalIssuesUrl" name="externalIssuesUrl" value="@repository.repository.externalIssuesUrl"/>
<input type="text" class="form-control" id="externalIssuesUrl" name="externalIssuesUrl" value="@repository.repository.options.externalIssuesUrl"/>
</fieldset>
<fieldset class="form-group margin">
<label class="checkbox" for="enableWiki">
<input type="checkbox" id="enableWiki" name="enableWiki"@if(repository.repository.enableWiki){ checked}/>
Wiki<br>
<div class="normal muted">
Provides a simple solution to manage documents. All users who can look this repository can read and collaborators can edit pages.
</div>
</label>
<label class="checkbox" for="allowWikiEditing">
<input type="checkbox" id="allowWikiEditing" name="allowWikiEditing"@if(repository.repository.allowWikiEditing){ checked}/>
Allow read-only users to edit Wiki pages<br>
</label>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Wiki</div>
<div class="panel-body">
<fieldset class="form-group">
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="DISABLE" @if(repository.repository.options.wikiOption == "DISABLE"){ checked}> Disables wiki
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="PRIVATE" @if(repository.repository.options.wikiOption == "PRIVATE"){ checked}> Developers can view, create and edit wiki pages
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="PUBLIC" @if(repository.repository.options.wikiOption == "PUBLIC"){ checked}> Developers and guests can view, create and edit wiki pages
</label>
</div>
<div class="radio for-public-repo">
<label>
<input type="radio" name="wikiOption" value="ALL" @if(repository.repository.options.issuesOption == "ALL"){ checked}> All users can view, create and comment on isues
</label>
</div>
<label for="externalWikiUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external Wiki for this project)</span>
</label>
<input type="text" class="form-control" id="externalWikiUrl" name="externalWikiUrl" value="@repository.repository.externalWikiUrl"/>
<input type="text" class="form-control" id="externalWikiUrl" name="externalWikiUrl" value="@repository.repository.options.externalWikiUrl"/>
</fieldset>
</div>
</div>
@@ -86,15 +122,25 @@
<script>
$(function(){
updateFeatures();
$('#enableIssues, #enableWiki').click(function(){
$('input[name=isPrivate], input[name=issuesOption], input[name=wikiOption]').click(function(){
updateFeatures();
});
});
function updateFeatures() {
$('#externalIssuesUrl').prop('disabled', $('#enableIssues').prop('checked'));
$('#allowWikiEditing').prop('disabled', !$('#enableWiki').prop('checked'));
$('#externalWikiUrl').prop('disabled', $('#enableWiki').prop('checked'));
if($('input[name=isPrivate]:checked').val() == 'false'){
$('.for-public-repo').show();
} else {
if($('input[name=issuesOption]:checked').val() == 'ALL'){
$('input[name=issuesOption][value=PUBLIC]').prop('checked', true);
}
if($('input[name=wikiOption]:checked').val() == 'ALL'){
$('input[name=wikiOption][value=PUBLIC]').prop('checked', true);
}
$('.for-public-repo').hide();
}
$('#externalIssuesUrl').prop('disabled', $('input[name=issuesOption]:checked').val() != 'DISABLE');
$('#externalWikiUrl').prop('disabled', $('input[name=wikiOption]:checked').val() != 'DISABLE');
}
</script>

View File

@@ -3,7 +3,7 @@
to: String,
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean,
isEditable: Boolean,
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@@ -27,7 +27,7 @@
<div class="pull-left">
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
</div>
@if(hasWritePermission){
@if(isEditable){
<div>
@if(pageName.isDefined){
<a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>

View File

@@ -44,7 +44,7 @@
</form>
}
}
<script>
<script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script>
$(function(){
try {
$('.clickable').dropzone({

View File

@@ -1,21 +1,20 @@
@(pageName: Option[String],
commits: List[gitbucket.core.util.JGitUtil.CommitInfo],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
isEditable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
<div class="pull-right">
@if(pageName.isEmpty){
@if(context.loginAccount.isDefined){
<a class="btn btn-small" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
} else {
@if(context.loginAccount.isDefined){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
@if(isEditable) {
<div class="pull-right">
@if(pageName.isEmpty) {
<a class="btn btn-small" href="@helpers.url(repository)/wiki/_new">New Page</a>
} else {
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</div>
}
</div>
<h1 class="wiki-title">
@if(pageName.isEmpty){
<span class="muted">History</span>

View File

@@ -2,7 +2,7 @@
page: gitbucket.core.service.WikiService.WikiPageInfo,
pages: List[String],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean,
isEditable: Boolean,
sidebar: Option[gitbucket.core.service.WikiService.WikiPageInfo],
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -10,12 +10,13 @@
@gitbucket.core.html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
<div>
@if(hasWritePermission){
<div class="pull-right">
<div class="pull-right">
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_history">Page History</a>
@if(isEditable){
<a class="btn btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
</div>
}
}
</div>
<h1 class="body-title">@pageName</h1>
<div>
<span class="muted"><strong>@page.committer</strong> edited this page @gitbucket.core.helper.html.datetimeago(page.time)</span>
@@ -48,13 +49,13 @@
}
@sidebar.map { sidebarPage =>
<div class="wiki-sidebar">
@if(hasWritePermission){
@if(isEditable){
<a href="@helpers.url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@helpers.markdown(sidebarPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
@if(isEditable){
<a class="button-link" href="@helpers.url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom sidebar</div>
</a>
@@ -72,7 +73,7 @@
</div>
}
</div>
<div style="margin-right: 260px;">
<div class="wiki-main">
<div class="markdown-body">
@helpers.markdown(
markdown = page.content,
@@ -87,13 +88,13 @@
</div>
@footer.map { footerPage =>
<div class="wiki-sidebar wiki-footer">
@if(hasWritePermission){
@if(isEditable){
<a href="@helpers.url(repository)/wiki/_Footer/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@helpers.markdown(footerPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
@if(isEditable){
<a class="button-link" href="@helpers.url(repository)/wiki/_Footer/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom footer</div>
</a>

View File

@@ -1,6 +1,6 @@
@(pages: List[String],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
isEditable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
@@ -10,7 +10,7 @@
</li>
<li class="pull-right">
<div class="btn-group">
@if(hasWritePermission){
@if(isEditable){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</div>

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