Compare commits

...

122 Commits
3.11 ... quill

Author SHA1 Message Date
Naoki Takezoe
8a9588f17f Moving to Quill 2016-03-10 18:30:48 +09:00
Naoki Takezoe
682f3a4c10 Moving to Quill 2016-03-10 16:40:25 +09:00
Naoki Takezoe
dad29d93c2 Moving to Quill 2016-03-10 13:27:02 +09:00
Naoki Takezoe
fc99de8a65 Merge branch 'master' into quill
# Conflicts:
#	build.sbt
#	src/main/scala/gitbucket/core/controller/AccountController.scala
#	src/main/scala/gitbucket/core/controller/UserManagementController.scala
2016-03-10 01:51:35 +09:00
Naoki Takezoe
ea7c8e62de Bump markedj to 1.0.7 2016-03-10 01:40:49 +09:00
Naoki Takezoe
b84421723b Merge pull request #1143 from mcs07/master
Fix special character encoding for blob links
2016-03-10 01:38:24 +09:00
Naoki Takezoe
9a42b93d1f Merge pull request #1140 from gitbucket/separate_api_controller
Separate API controller to improve routing performance
2016-03-10 01:34:56 +09:00
Matt Swain
e162cd956a Fix special character encoding for blob links 2016-03-08 12:37:58 +00:00
Naoki Takezoe
bc4af8e7c1 Finished to move AccountService to quill 2016-03-07 10:05:51 +09:00
Naoki Takezoe
cb3a79c9b3 Move some methods of AccountService to quill 2016-03-06 14:20:32 +09:00
Naoki Takezoe
b775ce157f Fix GroupMember model 2016-03-06 14:04:27 +09:00
Naoki Takezoe
70e2079c7f Update release.md 2016-03-06 12:58:44 +09:00
Naoki Takezoe
e8737d263a Merge pull request #1139 from ritschwumm/wip/sha256
generate sha-256 checksum
2016-03-06 12:57:29 +09:00
Naoki Takezoe
4c4b08f1b8 Merge pull request #1127 from McFoggy/issue-1117
add X-Hub-Signature security to wekhooks
2016-03-06 12:51:54 +09:00
Matthieu Brouillard
c7e1edf262 replace null by None 2016-03-05 21:12:51 +01:00
Naoki Takezoe
876bb396fd Fix testcase 2016-03-06 03:07:33 +09:00
Naoki Takezoe
a6788f858f Remove ControllerBase dependency from HandleCommentService 2016-03-06 03:04:45 +09:00
Naoki Takezoe
b263764730 Move createIssueComment() to IssuesService 2016-03-06 00:20:35 +09:00
Naoki Takezoe
1b1bd371a4 Fix testcase 2016-03-05 20:11:04 +09:00
Naoki Takezoe
f194a08cfe Separate API controller to improve routing performance 2016-03-05 19:47:27 +09:00
Naoki Takezoe
1211bfc7be Merge UserManagementController to SystemSettingsController to reduce filter mapping 2016-03-05 11:31:59 +09:00
Naoki Takezoe
eab7011e0f Merge remote-tracking branch 'origin/master' 2016-03-05 10:52:43 +09:00
Naoki Takezoe
6f30ffa865 Fixup 2016-03-05 10:51:45 +09:00
Naoki Takezoe
de3026248c Bump markedj to 1.0.7-SNAPSHOT 2016-03-05 10:31:27 +09:00
Herr Ritschwumm
413e75be5a generate checksums without ivy 2016-03-04 14:32:28 +01:00
Herr Ritschwumm
6a8ec18f9a generate sha256 checksum 2016-03-04 14:06:28 +01:00
Herr Ritschwumm
5b1b2ef3d7 tabs to spaces 2016-03-04 13:55:48 +01:00
Naoki Takezoe
9a705c62bf Merge pull request #1137 from marklacroix/fix-issue-comment-typo
Fix typo in issue comment list
2016-03-04 09:23:22 +09:00
Mark LaCroix
b103180bf6 Fix typo in issue comment list 2016-03-03 15:38:10 -05:00
Naoki Takezoe
536a0d3fe2 Merge pull request #1126 from gitbucket/add-test-info-to-run-doc
add info to run tests
2016-03-03 18:38:57 +09:00
Naoki Takezoe
cfcd250914 Introduction to quill 2016-03-03 18:36:52 +09:00
Matthieu Brouillard
356202e28a integrate xhub4j, fixes #1117 2016-03-02 22:25:21 +01:00
Naoki Takezoe
6db36e12b5 Merge pull request #1132 from McFoggy/issue-1128
correct path to CONTRIBUTING file, fixes #1128
2016-03-01 21:17:06 +09:00
Matthieu Brouillard
bfcd5a2855 correct path to CONTRIBUTING file, fixes #1128 2016-03-01 09:06:27 +01:00
Matthieu Brouillard
e218b52b78 add info to run tests 2016-03-01 01:10:19 +01:00
Naoki Takezoe
46998dc1fa Update release.md 2016-02-27 04:56:48 +09:00
Naoki Takezoe
977f856854 Update README.md 2016-02-27 04:52:36 +09:00
Naoki Takezoe
da2a7bf77d Fix comment editing in pull request diff view 2016-02-27 04:48:46 +09:00
Naoki Takezoe
3da3a048f0 Fix width of previewable editing forms 2016-02-27 02:56:22 +09:00
Naoki Takezoe
7b5b453e56 (refs #1123)Fix page list style 2016-02-26 21:48:49 +09:00
Naoki Takezoe
c18f95edf8 Merge pull request #1120 from lidice/adjust-image-width
Adjust img width in content box to max-width:100%
2016-02-25 13:23:24 +09:00
Naoki Takezoe
71cf043f56 Merge pull request #1119 from lidice/fix-unmatched-tag
(fixes #1118)Remove duplicated <script>
2016-02-25 13:22:17 +09:00
lidice
a31e4b5897 Adjust img width in content box to max-width:100% 2016-02-23 19:52:44 +09:00
lidice
1679da4abe (fixes #1118)Remove duplicated <script> 2016-02-22 11:55:31 +09:00
Naoki Takezoe
505bc71f9a Fix width of wiki page editing form 2016-02-22 08:57:23 +09:00
Naoki Takezoe
4bc057c653 Fix broken presentation 2016-02-22 02:17:41 +09:00
Naoki Takezoe
8eee13d7aa Merge some controllers because a large amount mapping causes performance issue 2016-02-22 01:33:38 +09:00
Naoki Takezoe
8981e339b4 Disable new pull request button if user does not signed-in 2016-02-21 16:00:14 +09:00
Naoki Takezoe
e1dd5dd057 Merged branch master into master 2016-02-21 15:58:46 +09:00
Naoki Takezoe
cb64f8eab8 Replace new issue button with new pull request button 2016-02-21 15:56:12 +09:00
Naoki Takezoe
c47d50d0df Fix pull request guide 2016-02-21 15:36:41 +09:00
Naoki Takezoe
1f46da2273 Merge pull request #1116 from McFoggy/templates
addition of issues & PR templates
2016-02-21 12:28:40 +09:00
Matthieu Brouillard
06fc26cd06 addition of issues & PR templates 2016-02-20 21:45:18 +01:00
Naoki Takezoe
3a4f9b9027 Merge pull request #1112 from oohira/fix/margin-after-octicon
Add margin after octicon
2016-02-21 03:51:11 +09:00
Naoki Takezoe
f98c849c7c Merge pull request #1115 from lidice/fix-label-color-format
(fixes #1114)Add Colorpicker options that to force hex format
2016-02-21 03:49:42 +09:00
Naoki Takezoe
aa0bd5b34a Update version to 3.12 2016-02-20 23:02:00 +09:00
Naoki Takezoe
b52e904ed1 Update CONTRIBUTING.md 2016-02-20 20:52:56 +09:00
lidice
70e0dcf99d (fixes #1114)Add Colorpicker options that to force hex format 2016-02-19 20:20:26 +09:00
Naoki Takezoe
56bb20dfd2 (refs #1113)Improve printing styles 2016-02-19 15:48:14 +09:00
oohira
7d7d2f488d Add margin after octicon 2016-02-17 23:42:51 +09:00
Naoki Takezoe
72affd67b9 Merge pull request #1108 from gitbucket/new-ui
New GitHub UI and Mobile support
2016-02-17 03:04:55 +09:00
Naoki Takezoe
0cf1f43deb Adjust issue / comment form 2016-02-16 17:34:21 +09:00
Naoki Takezoe
8494c682a7 Fix search box style for mobile 2016-02-16 15:02:34 +09:00
Naoki Takezoe
1af5611159 Mobile view improvement 2016-02-16 02:51:09 +09:00
Naoki Takezoe
4d39f63ef7 Tweak header buttons 2016-02-16 02:36:45 +09:00
Naoki Takezoe
120d1c2fff Implement repository url selector 2016-02-16 01:41:48 +09:00
Naoki Takezoe
62e9c0358a Adjust new file, new pull request button and others 2016-02-15 23:26:56 +09:00
Naoki Takezoe
5a90848c75 Remove unused code 2016-02-15 09:20:36 +09:00
Naoki Takezoe
760d443f74 Tweak top margin of contents 2016-02-15 09:11:57 +09:00
Naoki Takezoe
5ee0e75dfe Implementing new header parts 2016-02-15 02:21:00 +09:00
Naoki Takezoe
3b4d2d6f91 Fix header style 2016-02-15 01:02:35 +09:00
Naoki Takezoe
dfaabeb41d Move sidemenu to header 2016-02-14 23:41:07 +09:00
Naoki Takezoe
0fae2dac35 Merge pull request #1097 from ritschwumm/patch-3
Java 8 is a new requirement
2016-02-13 10:23:47 +09:00
Naoki Takezoe
4db4fe28b4 Merge pull request #1102 from ritschwumm/wip/name
rename file to the name of the type within
2016-02-13 10:19:08 +09:00
Naoki Takezoe
5b87efa032 (refs #1084)Remove RepositoryUrls 2016-02-13 10:16:11 +09:00
Herr Ritschwumm
3ad609bad7 rename file to the name of the type within 2016-02-13 01:24:04 +01:00
Naoki Takezoe
8145cba111 Merge branch 'wip/baseurl' of https://github.com/ritschwumm/gitbucket into ritschwumm-wip/baseurl 2016-02-12 23:15:59 +09:00
Herr Ritschwumm
24b9a9a12c remove RepoBase by moving RepositoryUrls construction into Context 2016-02-11 23:24:09 +01:00
Herr Ritschwumm
ee7220ebd2 move SshAddress into the SystemSettingsService object 2016-02-11 22:55:22 +01:00
Naoki Takezoe
8fb72fd55e (refs #982)Provide fixed url for pull request tabs 2016-02-09 18:14:03 +09:00
Naoki Takezoe
a1eded2d9a Merge pull request #1091 from oohira/fix/review-comment-box-border
Fix bug that border of review comment box is not shown
2016-02-09 13:58:18 +09:00
Naoki Takezoe
7f184e1126 Merge pull request #1100 from oohira/fix/label-duplicate-error
Fix bug that label duplicate check is wrong
2016-02-09 13:43:47 +09:00
oohira
09aafbcce1 Fix wrong query to find the specified label 2016-02-08 23:40:17 +09:00
Naoki Takezoe
7f5024a746 Change javac option to require Java8 2016-02-08 01:11:54 +09:00
Naoki Takezoe
8fec0870a8 Merge pull request #1098 from nus/fix-hidden-pull-requests
Fix some hidden pull requests
2016-02-08 01:03:41 +09:00
Naoki Takezoe
a8d2afaff7 Merge pull request #1099 from gitbucket/scalatest
Move to ScalaTest from Specs2
2016-02-07 00:02:46 +09:00
Naoki Takezoe
8fd92f1c2f Fix compilation error in testcase 2016-02-06 23:39:57 +09:00
Naoki Takezoe
419ea16ead Remove Specs2 dependency 2016-02-06 22:27:23 +09:00
Naoki Takezoe
e72d808a3c Migrating testcase to ScalaTest 2016-02-06 22:10:20 +09:00
Naoki Takezoe
44e8c0a9be Migrating testcase to ScalaTest 2016-02-05 23:00:35 +09:00
Naoki Takezoe
e2c39d7815 Merged branch master into scalatest 2016-02-04 01:51:29 +09:00
Yota Ichino
687cd54f9a Fix some hidden pull requests
IssuesService.IssueLimit value is equalized with
PullRequestService.PullRequestLimit value by this commit.

If without this, some pull requests are hidden.
For example, inspite of 30 pull requests are exists,
pull request page shows 25.
2016-02-03 16:43:49 +00:00
Naoki Takezoe
911754e1dc Migrating testcase to ScalaTest 2016-02-04 00:40:58 +09:00
Naoki Takezoe
0067cbce6f Fix failed test 2016-02-04 00:37:50 +09:00
Naoki Takezoe
f40f8427aa Replace === with == 2016-02-04 00:27:00 +09:00
oohira
98ceff2391 Fix bug that border of review comment box is not shown 2016-02-04 00:04:07 +09:00
Naoki Takezoe
642a51a208 Merge pull request #1096 from nus/fix-invisible-closed-label
Fix invisible closed label
2016-02-03 23:41:37 +09:00
Naoki Takezoe
9ec7c321d8 Merge pull request #1095 from oohira/link-avatar-image-to-profile-page
Link avatar image to the user's profile page
2016-02-03 23:33:57 +09:00
Naoki Takezoe
a3c419b6f5 Merge pull request #1086 from oohira/fix/invalid-repos-owner-url
Fix repository owner link URL
2016-02-03 23:08:55 +09:00
Naoki Takezoe
15c28cffa4 Merge branch 'ritschwumm-patch-1' 2016-02-03 22:37:40 +09:00
Naoki Takezoe
f4d0f16481 (refs #1083)Bump sbt launcher 2016-02-03 22:37:09 +09:00
Naoki Takezoe
45535e4fdf Merge branch 'patch-1' of https://github.com/ritschwumm/gitbucket into ritschwumm-patch-1 2016-02-03 22:34:30 +09:00
ritschwumm
64635c5dc6 Java 8 is a new requirement 2016-02-03 01:19:29 +01:00
Yota Ichino
2fd95c7f1a Fix invisible closed label
Closed label has label-important attribute which
was not defined in gitbucket.css, so that define
the attribute.
2016-02-02 15:35:45 +00:00
oohira
eb6da85183 Link avatar image to the user's profile page 2016-02-02 23:24:05 +09:00
Naoki Takezoe
bcc05f021c Migrating testcases to ScalaTest from Specs2 2016-02-02 00:36:08 +09:00
Naoki Takezoe
d58ed55c3a Merged branch ritschwumm-wip/escape into master 2016-01-31 12:26:37 +09:00
Naoki Takezoe
057f029c80 (refs #1085)Remove var by replacing for expression with foldLeft 2016-01-31 12:26:09 +09:00
Naoki Takezoe
c9a12ff913 Fix version extraction 2016-01-31 10:33:07 +09:00
oohira
66bf00b5d3 Fix repository owner link URL 2016-01-31 08:32:59 +09:00
Herr Ritschwumm
7ba3ca6f15 properly escape html characters in a description 2016-01-30 16:25:50 +01:00
Herr Ritschwumm
a1bacccc09 add tests, failing right now 2016-01-30 16:25:44 +01:00
ritschwumm
333eeb4bad update sbt to newest version 2016-01-30 11:24:23 +01:00
Herr Ritschwumm
3f2935612d feature: add settings for the ssh host 2016-01-30 11:15:07 +01:00
Herr Ritschwumm
4c87bdd959 refactor: make the settings alone responsible for ssh server location 2016-01-30 11:14:13 +01:00
Herr Ritschwumm
3543073150 cleanup: wiki urls could have been simpler 2016-01-30 11:12:11 +01:00
Herr Ritschwumm
e50fe604c2 cleanup: ... which in turns lets us avoid passing around the base url where we shouldn't need to know about it at all 2016-01-30 11:12:11 +01:00
Herr Ritschwumm
63369258bd preparation: move url generation from RepositoryInfo towards the gui 2016-01-30 11:12:07 +01:00
Herr Ritschwumm
e7c3376303 cleanup: baseUrl is not used here at all 2016-01-30 07:31:40 +01:00
Herr Ritschwumm
86163f66ce cleanup: jgit repo info does not have to know about urls in the gui 2016-01-30 07:31:26 +01:00
Herr Ritschwumm
518f0bfc28 cleanup: derive baseUrl from http request outside the SettingsService 2016-01-30 07:29:57 +01:00
Herr Ritschwumm
0a759f6127 cleanup: don't repeat yourself, calculate effective ssh port in one place only 2016-01-30 07:29:21 +01:00
134 changed files with 2919 additions and 2909 deletions

7
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Guideline for Issues
- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. At least, write subject in English.
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,19 @@
### Before submitting an issue to Gitbucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] searched for similar already existing issue
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
## Issue
**Impacted version**: xxxx
**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
**Problem description**:
- *be as explicit has you can*
- *describe the problem and its symptoms*
- *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)*
- *do your best to use a correct english (re-read yourself)*

8
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,8 @@
### Before submitting a pull-request to Gitbucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] rebased my branch over master
- [] verified that project is compiling
- [] verified that tests are passing
- [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)*
- [] [marked as closed](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct

View File

@@ -1,7 +0,0 @@
# Guideline for Issues
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
- Make sure check whether there is a same question or request in the past.
- When raise a new issue, write subject in **English** at least.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.

View File

@@ -60,6 +60,13 @@ Support
Release Notes Release Notes
-------- --------
### 3.12 - 27 Feb 2016
- New GitHub UI
- Improve mobile view
- Improve printing style
- Individual URL for pull request tabs
- SSH host configuration is separated from HTTP base URL
### 3.11 - 30 Jan 2016 ### 3.11 - 30 Jan 2016
- Upgrade Scalatra to 2.4 - Upgrade Scalatra to 2.4
- Sidebar and Footer for Wiki - Sidebar and Footer for Wiki
@@ -67,6 +74,7 @@ Release Notes
- Limit recent updated repositories list - Limit recent updated repositories list
- Issue actions look-alike GitHub - Issue actions look-alike GitHub
- Web API for labels - Web API for labels
- Requires Java 8
### 3.10 - 30 Dec 2015 ### 3.10 - 30 Dec 2015
- Move to Bootstrap3 - Move to Bootstrap3

View File

@@ -1,6 +1,6 @@
val Organization = "gitbucket" val Organization = "gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "3.11.0" val GitBucketVersion = "3.12.0"
val ScalatraVersion = "2.4.0" val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106" val JettyVersion = "9.3.6.v20151106"
@@ -10,13 +10,12 @@ sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.11.6" scalaVersion := "2.11.7"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/", "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
) )
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
@@ -26,7 +25,7 @@ libraryDependencies ++= Seq(
"org.json4s" %% "json4s-jackson" % "3.3.0", "org.json4s" %% "json4s-jackson" % "3.3.0",
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0", "io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
"commons-io" % "commons-io" % "2.4", "commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.6", "io.github.gitbucket" % "markedj" % "1.0.7",
"org.apache.commons" % "commons-compress" % "1.10", "org.apache.commons" % "commons-compress" % "1.10",
"org.apache.commons" % "commons-email" % "1.4", "org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1", "org.apache.httpcomponents" % "httpclient" % "4.5.1",
@@ -39,12 +38,14 @@ libraryDependencies ++= Seq(
"com.mchange" % "c3p0" % "0.9.5.2", "com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.3.0", "com.typesafe" % "config" % "1.3.0",
"com.typesafe.akka" %% "akka-actor" % "2.3.14", "com.typesafe.akka" %% "akka-actor" % "2.3.14",
"io.getquill" %% "quill-jdbc" % "0.4.1",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"), "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test", "junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.specs2" %% "specs2-junit" % "3.6.6" % "test" "org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
) )
// Twirl settings // Twirl settings
@@ -52,7 +53,7 @@ play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings // Compiler settings
scalacOptions := Seq("-deprecation", "-language:postfixOps") scalacOptions := Seq("-deprecation", "-language:postfixOps")
javacOptions in compile ++= Seq("-target", "7", "-source", "7") javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console") testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test" javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
@@ -82,36 +83,36 @@ jrebelSettings
// Create executable war file // Create executable war file
val executableConfig = config("executable").hide val executableConfig = config("executable").hide
Keys.ivyConfigurations += executableConfig Keys.ivyConfigurations += executableConfig
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
) )
val executableKey = TaskKey[File]("executable") val executableKey = TaskKey[File]("executable")
executableKey := { executableKey := {
import org.apache.ivy.util.ChecksumHelper import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest } import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName } import java.util.jar.Attributes.{ Name => AttrName }
val workDir = Keys.target.value / "executable" val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war" val warName = Keys.name.value + ".war"
val log = streams.value.log val log = streams.value.log
log info s"building executable webapp in ${workDir}" log info s"building executable webapp in ${workDir}"
// initialize temp directory // initialize temp directory
val temp = workDir / "webapp" val temp = workDir / "webapp"
IO delete temp IO delete temp
// include jetty classes // include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
jettyJars foreach { jar => jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) => IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") || (name startsWith "javax/") ||
@@ -120,31 +121,34 @@ executableKey := {
} }
// include original war file // include original war file
val warFile = (Keys.`package`).value val warFile = (Keys.`package`).value
IO unzip (warFile, temp) IO unzip (warFile, temp)
// include launcher classes // include launcher classes
val classDir = (Keys.classDirectory in Compile).value val classDir = (Keys.classDirectory in Compile).value
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
launchClasses foreach { name => launchClasses foreach { name =>
IO copyFile (classDir / name, temp / name) IO copyFile (classDir / name, temp / name)
} }
// zip it up // zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF") IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
val manifest = new JarManifest val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName val outputFile = workDir / warName
IO jar (contentMappings, outputFile, manifest) IO jar (contentMappings, outputFile, manifest)
// generate checksums // generate checksums
Seq("md5", "sha1") foreach { algorithm => Seq(
IO.write( "md5" -> "MD5",
workDir / (warName + "." + algorithm), "sha1" -> "SHA-1",
ChecksumHelper computeAsString (outputFile, algorithm) "sha256" -> "SHA-256"
) )
.foreach { case (extension, algorithm) =>
val checksumFile = workDir / (warName + "." + extension)
Checksums generate (outputFile, checksumFile, algorithm)
} }
// done // done
@@ -153,7 +157,7 @@ executableKey := {
} }
/* /*
Keys.artifact in (Compile, executableKey) ~= { Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war")) _ copy (`type` = "war", extension = "war"))
} }
addArtifact(Keys.artifact in (Compile, executableKey), executableKey) addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/ */

View File

@@ -32,3 +32,11 @@ $ sbt executable
``` ```
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact. at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
Run tests spec
---------
To run the full serie of tests, run the following command:
```
sbt test
```

View File

@@ -6,15 +6,14 @@ Update version number
Note to update version number in files below: Note to update version number in files below:
### project/build.scala ### build.sbt
```scala ```scala
object MyBuild extends Build { val Organization = "gitbucket"
val Organization = "gitbucket" val Name = "gitbucket"
val Name = "gitbucket" val GitBucketVersion = "3.12.0" // <---- update version!!
val Version = "3.3.0" // <---- update version!! val ScalatraVersion = "2.4.0"
val ScalaVersion = "2.11.6" val JettyVersion = "9.3.6.v20151106"
val ScalatraVersion = "2.3.1"
``` ```
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala ### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
@@ -26,8 +25,8 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current GitBucket version. * The history of versions. A head of this sequence is the current GitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 3), // <---- add this line!! new Version(3, 12), // <---- add this line!!
new Version(3, 2), new Version(3, 11),
``` ```
Generate release files Generate release files

34
project/Checksums.scala Normal file
View File

@@ -0,0 +1,34 @@
import java.security.MessageDigest;
import scala.annotation._
import sbt._
import sbt.Using._
object Checksums {
private val bufferSize = 2048
def generate(source:File, target:File, algorithm:String):Unit =
IO write (target, compute(source, algorithm))
def compute(file:File, algorithm:String):String =
hex(raw(file, algorithm))
def raw(file:File, algorithm:String):Array[Byte] =
(Using fileInputStream file) { is =>
val md = MessageDigest getInstance algorithm
val buf = new Array[Byte](bufferSize)
md.reset()
@tailrec
def loop() {
val len = is read buf
if (len != -1) {
md update (buf, 0, len)
loop()
}
}
loop()
md.digest()
}
def hex(bytes:Array[Byte]):String =
bytes map { it => "%02x" format (it.toInt & 0xff) } mkString ""
}

View File

@@ -1 +1 @@
sbt.version=0.13.8 sbt.version=0.13.9

View File

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

View File

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

2
sbt.sh
View File

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

View File

@@ -0,0 +1,8 @@
db.dataSourceClassName="org.h2.jdbcx.JdbcDataSource"
db.dataSource.url="jdbc:h2:~/.gitbucket/data;MVCC=true"
db.dataSource.user="sa"
db.dataSource.password="sa"
#db.dataSource.cachePrepStmts=true
#db.dataSource.prepStmtCacheSize=250
#db.dataSource.prepStmtCacheSqlLimit=2048
#db.connectionTimeout=30000

View File

@@ -0,0 +1 @@
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);

View File

@@ -27,12 +27,10 @@ class ScalatraBootstrap extends LifeCycle {
} }
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/") context.mount(new ApiController, "/api/v3")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SystemSettingsController, "/admin")
context.mount(new DashboardController, "/*") context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new PluginsController, "/*")
context.mount(new AccountController, "/*") context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*") context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*") context.mount(new WikiController, "/*")

View File

@@ -39,7 +39,7 @@ object ApiRepository{
description = repository.description.getOrElse(""), description = repository.description.getOrElse(""),
watchers = 0, watchers = 0,
forks = forkedCount, forks = forkedCount,
`private` = repository.isPrivate, `private` = repository.`private`,
default_branch = repository.defaultBranch, default_branch = repository.defaultBranch,
owner = owner owner = owner
)(urlIsHtmlUrl) )(urlIsHtmlUrl)

View File

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

View File

@@ -1,7 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.account.html import gitbucket.core.account.html
import gitbucket.core.api._
import gitbucket.core.helper import gitbucket.core.helper
import gitbucket.core.model.GroupMember import gitbucket.core.model.GroupMember
import gitbucket.core.service._ import gitbucket.core.service._
@@ -14,22 +13,19 @@ import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccessTokenService with WebHookService with AccessTokenService with WebHookService with RepositoryCreationService
trait AccountControllerBase extends AccountManagementControllerBase { trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccessTokenService with WebHookService => with AccessTokenService with WebHookService with RepositoryCreationService =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String]) url: Option[String], fileId: Option[String])
@@ -118,23 +114,23 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Public Activity // Public Activity
case "activity" => case "activity" =>
gitbucket.core.account.html.activity(account, gitbucket.core.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), if(account.groupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true)) getActivitiesByUser(userName, true))
// Members // Members
case "members" if(account.isGroupAccount) => { case "members" if(account.groupAccount) => {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
gitbucket.core.account.html.members(account, members.map(_.userName), gitbucket.core.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.manager }))
} }
// Repositories // Repositories
case _ => { case _ => {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
gitbucket.core.account.html.repositories(account, gitbucket.core.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName), if(account.groupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)), getVisibleRepositories(context.loginAccount, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.manager }))
} }
} }
} getOrElse NotFound } getOrElse NotFound
@@ -156,25 +152,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
} }
/**
* https://developer.github.com/v3/users/#get-a-single-user
*/
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
}
/**
* https://developer.github.com/v3/users/#get-the-authenticated-user
*/
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
}
get("/:userName/_edit")(oneselfOnly { get("/:userName/_edit")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
@@ -213,7 +190,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// removeUserRelatedData(userName) // removeUserRelatedData(userName)
removeUserRelatedData(userName) removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true)) updateAccount(account.copy(removed = true))
} }
session.invalidate session.invalidate
@@ -366,8 +343,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/ */
post("/new", newRepositoryForm)(usersOnly { form => post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){ LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ if(getRepository(form.owner, form.name).isEmpty){
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
} }
// redirect to the repository // redirect to the repository
@@ -375,54 +352,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
}) })
/**
* Create user repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/user/repos")(usersOnly {
val owner = context.loginAccount.get.userName
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
"A repository with this name already exists on this account",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
/**
* Create group repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/orgs/:org/repos")(managersOnly {
val groupName = params("org")
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
"A repository with this name already exists for this group",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
@@ -431,7 +360,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case _: List[String] => case _: List[String] =>
val managerPermissions = groups.map { group => val managerPermissions = groups.map { group =>
val members = getGroupMembers(group) val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.manager })
} }
helper.html.forkrepository( helper.html.forkrepository(
repository, repository,
@@ -447,7 +376,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val accountName = form.accountName val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){ LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined || if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists // redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}") redirect(s"/${accountName}/${repository.name}")
@@ -456,11 +385,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val originUserName = repository.repository.originUserName.getOrElse(repository.owner) val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository( insertRepository(
repositoryName = repository.name, repositoryName = repository.name,
userName = accountName, userName = accountName,
description = repository.repository.description, description = repository.repository.description,
isPrivate = repository.repository.isPrivate, isPrivate = repository.repository.`private`,
originRepositoryName = Some(originRepositoryName), originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName), originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name), parentRepositoryName = Some(repository.name),
@@ -469,7 +398,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Add collaborators for group repository // Add collaborators for group repository
val ownerAccount = getAccountByUserName(accountName).get val ownerAccount = getAccountByUserName(accountName).get
if(ownerAccount.isGroupAccount){ if(ownerAccount.groupAccount){
getGroupMembers(accountName).foreach { member => getGroupMembers(accountName).foreach { member =>
addCollaborator(accountName, repository.name, member.userName) addCollaborator(accountName, repository.name, member.userName)
} }
@@ -496,68 +425,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
}) })
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
val ownerAccount = getAccountByUserName(owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(name, owner, description, isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
if(createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(description.nonEmpty){
name + "\n" +
"===============\n" +
"\n" +
description.get
} else {
name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, owner, name)
// Record activity
recordCreateRepositoryActivity(owner, name, loginUserName)
}
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){ private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None

View File

@@ -0,0 +1,389 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.model._
import gitbucket.core.service.IssuesService.IssueSearchCondition
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
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import org.eclipse.jgit.api.Git
import org.scalatra.{NoContent, UnprocessableEntity, Created}
import scala.collection.JavaConverters._
class ApiController extends ApiControllerBase
with RepositoryService
with AccountService
with ProtectedBranchService
with IssuesService
with LabelsService
with PullRequestService
with CommitStatusService
with RepositoryCreationService
with HandleCommentService
with WebHookService
with WebHookPullRequestService
with WebHookIssueCommentService
with WikiService
with ActivityService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator
trait ApiControllerBase extends ControllerBase {
self: RepositoryService
with AccountService
with ProtectedBranchService
with IssuesService
with LabelsService
with PullRequestService
with CommitStatusService
with RepositoryCreationService
with HandleCommentService
with OwnerAuthenticator
with UsersAuthenticator
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator =>
/**
* https://developer.github.com/v3/users/#get-a-single-user
*/
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
}
/**
* https://developer.github.com/v3/users/#get-the-authenticated-user
*/
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
}
/**
* Create user repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/user/repos")(usersOnly {
val owner = context.loginAccount.get.userName
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name).isEmpty){
createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
"A repository with this name already exists on this account",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
/**
* Create group repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/orgs/:org/repos")(managersOnly {
val groupName = params("org")
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name).isEmpty){
createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
"A repository with this name already exists for this group",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
} yield {
if(protection.enabled){
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
})
/**
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
* but not enabled.
*/
get("/api/v3/rate_limit"){
contentType = formats("json")
// this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
}
/**
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
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)
})
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
*/
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
(issue, id) <- handleComment(issue, Some(body), repository, action)
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
}) getOrElse NotFound
})
/**
* List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/**
* Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
/**
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
))
}
}
}) getOrElse NotFound()
})
/**
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)))
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
}
} getOrElse NotFound()
}
}) getOrElse NotFound()
})
/**
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/**
* https://developer.github.com/v3/pulls/#list-pull-requests
*/
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)) })
})
/**
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)))
}).getOrElse(NotFound)
})
/**
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
params("id").toIntOpt.flatMap{ issueId =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo)
val repoFullName = RepositoryName(repository)
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
JsonFormat(commits)
}
}
} getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/#get
*/
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
})
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
*/
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
(for{
ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
data <- extractFromJsonBody[CreateAStatus] if data.isValid
creator <- context.loginAccount
state <- CommitState.valueOf(data.state)
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
state, data.target_url, data.description, new java.util.Date(), creator)
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* legacy route
*/
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
listStatusesRoute.action()
}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
owner <- getAccountByUserName(repository.owner)
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) 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
}

View File

@@ -50,7 +50,7 @@ abstract class ControllerBase extends ScalatraFilter
if(account == null){ if(account == null){
// Redirect to login form // Redirect to login form
httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path)) httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
} else if(account.isAdmin){ } else if(account.administrator){
// H2 Console (administrators only) // H2 Console (administrators only)
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
@@ -180,7 +180,6 @@ abstract class ControllerBase extends ScalatraFilter
* Context object for the current request. * Context object for the current request.
*/ */
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
val path = settings.baseUrl.getOrElse(request.getContextPath) val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length) val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request) val baseUrl = settings.baseUrl(request)

View File

@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
html.issues( html.issues(

View File

@@ -2,35 +2,46 @@ package gitbucket.core.controller
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.helper.xml import gitbucket.core.helper.xml
import gitbucket.core.html
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService} import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator} import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
with UsersAuthenticator with ReferrerAuthenticator
trait IndexControllerBase extends ControllerBase { trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String) case class SignInForm(userName: String, password: String)
val form = mapping( val signinForm = mapping(
"userName" -> trim(label("Username", text(required))), "userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required))) "password" -> trim(label("Password", text(required)))
)(SignInForm.apply) )(SignInForm.apply)
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
if(loginAccount.isEmpty) { if(loginAccount.isEmpty) {
html.index(getRecentActivities(), gitbucket.core.html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
) )
} else { } else {
val loginUserName = loginAccount.get.userName val loginUserName = loginAccount.get.userName
@@ -39,9 +50,9 @@ trait IndexControllerBase extends ControllerBase {
visibleOwnerSet ++= loginUserGroups visibleOwnerSet ++= loginUserGroups
html.index(getRecentActivitiesByOwners(visibleOwnerSet), gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
) )
} }
} }
@@ -51,10 +62,10 @@ trait IndexControllerBase extends ControllerBase {
if(redirect.isDefined && redirect.get.startsWith("/")){ if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get flash += Keys.Flash.Redirect -> redirect.get
} }
html.signin() gitbucket.core.html.signin()
} }
post("/signin", form){ form => post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match { authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account) case Some(account) => signin(account)
case None => redirect("/signin") case None => redirect("/signin")
@@ -99,7 +110,7 @@ trait IndexControllerBase extends ControllerBase {
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray) Map("options" -> getAllUsers(false).filter(!_.groupAccount).map(_.userName).toArray)
) )
}) })
@@ -110,13 +121,32 @@ trait IndexControllerBase extends ControllerBase {
getAccountByUserName(params("userName")).isDefined getAccountByUserName(params("userName")).isDefined
}) })
/** // TODO Move to RepositoryViwerController?
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status post("/search", searchForm){ form =>
* but not enabled. redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
*/
get("/api/v3/rate_limit"){
contentType = formats("json")
// this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
} }
// TODO Move to RepositoryViwerController?
get("/:owner/:repository/search")(referrersOnly { repository =>
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => gitbucket.core.search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => gitbucket.core.search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})
} }

View File

@@ -1,8 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.issues.html import gitbucket.core.issues.html
import gitbucket.core.model.Issue
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
@@ -16,11 +14,11 @@ import org.scalatra.Ok
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService 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 CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
trait IssuesControllerBase extends ControllerBase { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService 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 CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
@@ -69,7 +67,7 @@ trait IssuesControllerBase extends ControllerBase {
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.groupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
@@ -78,22 +76,10 @@ trait IssuesControllerBase extends ControllerBase {
} }
}) })
/**
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
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)
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
html.create( html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.groupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name), getMilestones(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
@@ -128,7 +114,7 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(owner, name, issueId.toString).foreach { issue => getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// call web hooks // call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
@@ -150,7 +136,7 @@ trait IssuesControllerBase extends ControllerBase {
// update issue // update issue
updateIssue(owner, name, issue.issueId, title, issue.content) updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue.copy(title = title), title) createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
@@ -165,7 +151,7 @@ trait IssuesControllerBase extends ControllerBase {
// update issue // update issue
updateIssue(owner, name, issue.issueId, issue.title, content) updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse("")) createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
@@ -174,30 +160,22 @@ trait IssuesControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
redirect(s"/${repository.owner}/${repository.name}/${ val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") 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
}) })
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
*/
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
(issue, id) <- handleComment(issueId, Some(body), repository)()
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
}) getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
redirect(s"/${repository.owner}/${repository.name}/${ val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") 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
}) })
@@ -315,8 +293,16 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
action match { action match {
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } case Some("open") => executeBatch(repository) { issueId =>
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
handleComment(issue, None, repository, Some("reopen"))
}
}
case Some("close") => executeBatch(repository) { issueId =>
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
handleComment(issue, None, repository, Some("close"))
}
}
case _ => // TODO BadRequest case _ => // TODO BadRequest
} }
} }
@@ -373,99 +359,6 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title
if(getIssue(owner, repository, issueId).isDefined){
// Not add if refer comment already exist.
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
}
}
}
}
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) flatMap { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" if(issue.closed) => false ->
(Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = (content, action) match {
case (None, None) => None
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
}
// record comment activity if comment is entered
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
// extract references and create refer comment
content.map { content =>
createReferComment(owner, name, issue, content)
}
// 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){
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
}
}
// notifications
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issue, _){
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
}
}
action foreach {
f.toNotify(repository, issue, _){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
}
commentId.map( issue -> _ )
}
}
}
private def searchIssues(repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
@@ -487,7 +380,7 @@ trait IssuesControllerBase extends ControllerBase {
"issues", "issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ if(!getAccountByUserName(owner).exists(_.groupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted (getCollaborators(owner, repoName) :+ owner).sorted
} else { } else {
getCollaborators(owner, repoName) getCollaborators(owner, repoName)

View File

@@ -1,13 +1,12 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
import gitbucket.core.issues.labels.html import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok} import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService with LabelsService with IssuesService with RepositoryService with AccountService
@@ -32,26 +31,6 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
/**
* List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/**
* Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
html.edit(None, repository) html.edit(None, repository)
}) })
@@ -66,31 +45,6 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
/**
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
))
}
}
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository) html.edit(Some(label), repository)
@@ -107,50 +61,11 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
/**
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)))
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
}
} getOrElse NotFound()
}
}) getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok() Ok()
}) })
/**
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/** /**
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */

View File

@@ -1,11 +0,0 @@
package gitbucket.core.controller
import gitbucket.core.admin.plugins.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.AdminAuthenticator
class PluginsController extends ControllerBase with AdminAuthenticator {
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
})
}

View File

@@ -1,7 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.api._ import gitbucket.core.model.WebHook
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
import gitbucket.core.pulls.html import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService import gitbucket.core.service.MergeService
@@ -82,24 +81,6 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
}) })
/**
* https://developer.github.com/v3/pulls/#list-pull-requests
*/
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)) })
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -113,7 +94,7 @@ trait PullRequestsControllerBase extends ControllerBase {
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate), .sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId), getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.groupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
commits, commits,
@@ -126,47 +107,6 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound } getOrElse NotFound
}) })
/**
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
} yield {
JsonFormat(ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)))
}).getOrElse(NotFound)
})
/**
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
params("id").toIntOpt.flatMap{ issueId =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo)
val repoFullName = RepositoryName(repository)
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
JsonFormat(commits)
}
}
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -196,7 +136,7 @@ trait PullRequestsControllerBase extends ControllerBase {
issue, issue,
pullreq, pullreq,
repository, repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
} }
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -229,7 +169,7 @@ trait PullRequestsControllerBase extends ControllerBase {
if(branchProtection.needStatusCheck(loginAccount.userName)){ if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else { } else {
val repository = getRepository(owner, name, context.baseUrl).get val repository = getRepository(owner, name).get
LockUtil.lock(s"${owner}/${name}"){ LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch pullreq.branch
@@ -310,7 +250,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request // close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){ if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit => commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
@@ -343,7 +283,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val headBranch:Option[String] = params.get("head") val headBranch:Option[String] = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => { case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => getRepository(originUserName, originRepositoryName).map { originRepository =>
using( using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -384,12 +324,12 @@ trait PullRequestsControllerBase extends ControllerBase {
forkedRepository.repository.originRepositoryName forkedRepository.repository.originRepositoryName
} else { } else {
// Sibling repository // Sibling repository
getUserRepositories(originOwner, context.baseUrl).find { x => getUserRepositories(originOwner).find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName && x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}.map(_.repository.repositoryName) }.map(_.repository.repositoryName)
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -430,7 +370,7 @@ trait PullRequestsControllerBase extends ControllerBase {
originRepository, originRepository,
forkedRepository, forkedRepository,
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount),
(getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted, (getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.groupAccount) Nil else List(originRepository.owner))).sorted,
getMilestones(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name) getLabels(originRepository.owner, originRepository.name)
) )
@@ -457,7 +397,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} }
}; };
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) originRepository <- getRepository(originOwner, originRepositoryName)
) yield { ) yield {
using( using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -523,7 +463,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getIssue(owner, name, issueId.toString) foreach { issue => getIssue(owner, name, issueId.toString) foreach { issue =>
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// notifications // notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){ Notifier().toNotify(repository, issue, form.content.getOrElse("")){
@@ -535,19 +475,6 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
}) })
// TODO Same method exists in IssueController. Should it moved to IssueService?
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title
if(getIssue(owner, repository, issueId).isDefined){
// Not add if refer comment already exist.
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
}
}
}
}
/** /**
* Parses branch identifier and extracts owner and branch name as tuple. * Parses branch identifier and extracts owner and branch name as tuple.
* *
@@ -597,7 +524,7 @@ trait PullRequestsControllerBase extends ControllerBase {
"pulls", "pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page, page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ if(!getAccountByUserName(owner).exists(_.groupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted (getCollaborators(owner, repoName) :+ owner).sorted
} else { } else {
getCollaborators(owner, repoName) getCollaborators(owner, repoName)
@@ -611,14 +538,4 @@ trait PullRequestsControllerBase extends ControllerBase {
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
} }

View File

@@ -49,11 +49,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition // for web hook url addition
case class WebHookForm(url: String, events: Set[WebHook.Event]) case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String])
def webHookForm(update:Boolean) = mapping( def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook(update)))), "url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents "events" -> webhookEvents,
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(WebHookForm.apply) )(WebHookForm.apply)
// for transfer ownership // for transfer ownership
@@ -86,7 +87,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository.name, repository.name,
form.description, form.description,
repository.repository.parentUserName.map { _ => repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate repository.repository.`private`
} getOrElse form.isPrivate } getOrElse form.isPrivate
) )
// Change repository name // Change repository name
@@ -141,29 +142,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
} }
}) })
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
} yield {
if(protection.enabled){
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
})
/** /**
* Display the Collaborators page. * Display the Collaborators page.
*/ */
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
html.collaborators( html.collaborators(
getCollaborators(repository.owner, repository.name), getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount, getAccountByUserName(repository.owner).get.groupAccount,
repository) repository)
}) })
@@ -171,7 +156,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the collaborator. * Add the collaborator.
*/ */
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ if(!getAccountByUserName(repository.owner).get.groupAccount){
addCollaborator(repository.owner, repository.name, form.userName) addCollaborator(repository.owner, repository.name, form.userName)
} }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
@@ -181,7 +166,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the collaborator. * Add the collaborator.
*/ */
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ if(!getAccountByUserName(repository.owner).get.groupAccount){
removeCollaborator(repository.owner, repository.name, params("name")) removeCollaborator(repository.owner, repository.name, params("name"))
} }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
@@ -198,7 +183,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "") val webhook = WebHook(repository.owner, repository.name, "", None)
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
}) })
@@ -206,7 +191,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Add the web hook URL. * Add the web hook URL.
*/ */
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHook(repository.owner, repository.name, form.url, form.events) addWebHook(repository.owner, repository.name, form.url, form.events, form.token)
flash += "info" -> s"Webhook ${form.url} created" flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -235,7 +220,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
val url = params("url") val url = params("url")
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) val token = Some(params("token"))
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token)
val dummyPayload = { val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(repository.commitCount == 0) List.empty else git.log val commits = if(repository.commitCount == 0) List.empty else git.log
@@ -294,7 +280,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Update web hook settings. * Update web hook settings.
*/ */
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events) updateWebHook(repository.owner, repository.name, form.url, form.events, form.token)
flash += "info" -> s"webhook ${form.url} updated" flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -378,7 +364,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount) case Some(x) if(x.groupAccount)
=> Some("User does not exist.") => Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.") => Some("User can access this repository already.")

View File

@@ -2,7 +2,6 @@ package gitbucket.core.controller
import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html import gitbucket.core.repo.html
import gitbucket.core.helper import gitbucket.core.helper
@@ -13,7 +12,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState, WebHook} import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
@@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_) fileList(_)
}) })
/**
* https://developer.github.com/v3/repos/#get
*/
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
})
/** /**
* Displays the file list of the specified path and branch. * Displays the file list of the specified path and branch.
*/ */
@@ -160,65 +152,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
*/
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
(for{
ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
data <- extractFromJsonBody[CreateAStatus] if data.isValid
creator <- context.loginAccount
state <- CommitState.valueOf(data.state)
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
state, data.target_url, data.description, new java.util.Date(), creator)
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* legacy route
*/
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
listStatusesRoute.action()
}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
owner <- getAccountByUserName(repository.owner)
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository => get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head) val (branch, path) = splitPath(repository, multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
@@ -560,8 +493,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.forked( html.forked(
getRepository( getRepository(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name), repository.repository.originRepositoryName.getOrElse(repository.name)),
context.baseUrl),
getForkedRepositories( getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)), repository.repository.originRepositoryName.getOrElse(repository.name)),
@@ -759,8 +691,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.setTree(revCommit.getTree) .setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream) .setOutputStream(response.getOutputStream)
.call() .call()
Unit
} }
} }

View File

@@ -1,51 +0,0 @@
package gitbucket.core.controller
import gitbucket.core.search.html
import gitbucket.core.service._
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
import ControlUtil._
import Implicits._
import io.github.gitbucket.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})
}

View File

@@ -1,17 +1,24 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.admin.html import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, SystemSettingsService} import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService}
import gitbucket.core.util.AdminAuthenticator import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry
import SystemSettingsService._ import SystemSettingsService._
import gitbucket.core.util.Implicits._
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.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase { trait SystemSettingsControllerBase extends AccountManagementControllerBase {
self: AccountService with AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
@@ -23,6 +30,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())), "ssh" -> trim(label("SSH access", boolean())),
"sshHost" -> trim(label("SSH host", optional(text()))),
"sshPort" -> trim(label("SSH port", optional(number()))), "sshPort" -> trim(label("SSH port", optional(number()))),
"useSMTP" -> trim(label("SMTP", boolean())), "useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked("useSMTP", mapping( "smtp" -> optionalIfNotChecked("useSMTP", mapping(
@@ -50,9 +58,14 @@ trait SystemSettingsControllerBase extends ControllerBase {
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)) )(Ldap.apply))
)(SystemSettings.apply).verifying { settings => )(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){ Vector(
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") if(settings.ssh && settings.baseUrl.isEmpty){
} else Nil Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None,
if(settings.ssh && settings.sshHost.isEmpty){
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
} else None
).flatten
} }
private val pluginForm = mapping( private val pluginForm = mapping(
@@ -61,6 +74,61 @@ trait SystemSettingsControllerBase extends ControllerBase {
case class PluginForm(pluginIds: List[String]) case class PluginForm(pluginIds: List[String])
case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
case class EditUserForm(userName: String, password: Option[String], fullName: String,
mailAddress: String, isAdmin: Boolean, url: Option[String],
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"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()))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text())))
)(NewUserForm.apply)
val editUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
html.system(flash.get("info")) html.system(flash.get("info"))
}) })
@@ -68,20 +136,155 @@ trait SystemSettingsControllerBase extends ControllerBase {
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form) saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){ if (form.sshAddress != context.settings.sshAddress) {
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop() SshServer.stop()
for {
sshAddress <- form.sshAddress
baseUrl <- form.baseUrl
}
SshServer.start(sshAddress, baseUrl)
} }
flash += "info" -> "System settings has been updated." flash += "info" -> "System settings has been updated."
redirect("/admin/system") redirect("/admin/system")
}) })
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
})
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.groupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
html.userlist(users, members, includeRemoved)
})
get("/admin/users/_newuser")(adminOnly {
html.user(None)
})
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
html.user(getAccountByUserName(userName, true))
})
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName, true).map { account =>
if(form.isRemoved){
// Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
administrator = form.isAdmin,
url = form.url,
removed = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
get("/admin/users/_newgroup")(adminOnly {
html.usergroup(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
if(form.isRemoved){
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
} 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)
}
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
}
})
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
Some("You can't disable your account yourself")
else
None
}
}
}
} }

View File

@@ -1,204 +0,0 @@
package gitbucket.core.controller
import gitbucket.core.service.{RepositoryService, AccountService}
import gitbucket.core.admin.users.html
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator =>
case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
case class EditUserForm(userName: String, password: Option[String], fullName: String,
mailAddress: String, isAdmin: Boolean, url: Option[String],
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
members: String, clearImage: Boolean, isRemoved: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
"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()))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text())))
)(NewUserForm.apply)
val editUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" ,boolean())),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members)))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))),
"clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly {
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
val users = getAllUsers(includeRemoved)
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName).map(_.userName)
}.toMap
html.list(users, members, includeRemoved)
})
get("/admin/users/_newuser")(adminOnly {
html.user(None)
})
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
html.user(getAccountByUserName(userName, true))
})
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName, true).map { account =>
if(form.isRemoved){
// Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url,
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
})
get("/admin/users/_newgroup")(adminOnly {
html.group(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList)
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.members.split(",").map {
_.split(":") match {
case Array(userName, isManager) => (userName, isManager.toBoolean)
}
}.toList){ case (groupName, members) =>
getAccountByUserName(groupName, true).map { account =>
updateGroup(groupName, form.url, form.isRemoved)
if(form.isRemoved){
// Remove from GROUP_MEMBER
updateGroupMembers(form.groupName, Nil)
// Remove repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
deleteRepository(groupName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
}
} 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)
}
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
}
})
private def members: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
if(value.split(",").exists {
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
}) None else Some("Must select one manager at least.")
}
}
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
Some("You can't disable your account yourself")
else
None
}
}
}
}

View File

@@ -11,7 +11,7 @@ trait AccountComponent { self: Profile =>
val fullName = column[String]("FULL_NAME") val fullName = column[String]("FULL_NAME")
val mailAddress = column[String]("MAIL_ADDRESS") val mailAddress = column[String]("MAIL_ADDRESS")
val password = column[String]("PASSWORD") val password = column[String]("PASSWORD")
val isAdmin = column[Boolean]("ADMINISTRATOR") val administrator = column[Boolean]("ADMINISTRATOR")
val url = column[String]("URL") val url = column[String]("URL")
val registeredDate = column[java.util.Date]("REGISTERED_DATE") val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE")
@@ -19,7 +19,7 @@ trait AccountComponent { self: Profile =>
val image = column[String]("IMAGE") val image = column[String]("IMAGE")
val groupAccount = column[Boolean]("GROUP_ACCOUNT") val groupAccount = column[Boolean]("GROUP_ACCOUNT")
val removed = column[Boolean]("REMOVED") val removed = column[Boolean]("REMOVED")
def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) def * = (userName, fullName, mailAddress, password, administrator, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
} }
} }
@@ -28,12 +28,12 @@ case class Account(
fullName: String, fullName: String,
mailAddress: String, mailAddress: String,
password: String, password: String,
isAdmin: Boolean, administrator: Boolean,
url: Option[String], url: Option[String],
registeredDate: java.util.Date, registeredDate: java.util.Date,
updatedDate: java.util.Date, updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date], lastLoginDate: Option[java.util.Date],
image: Option[String], image: Option[String],
isGroupAccount: Boolean, groupAccount: Boolean,
isRemoved: Boolean removed: Boolean
) )

View File

@@ -35,7 +35,7 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.labelId === labelId) byRepository(userName, repositoryName) && (this.labelId === labelId)
def byLabel(owner: String, repository: String, labelName: String) = def byLabel(owner: String, repository: String, labelName: String) =
byRepository(userName, repositoryName) && (this.labelName === labelName.bind) byRepository(owner, repository) && (this.labelName === labelName.bind)
} }
trait MilestoneTemplate extends BasicTemplate { self: Table[_] => trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>

View File

@@ -8,13 +8,13 @@ trait GroupMemberComponent { self: Profile =>
class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") { class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") {
val groupName = column[String]("GROUP_NAME", O PrimaryKey) val groupName = column[String]("GROUP_NAME", O PrimaryKey)
val userName = column[String]("USER_NAME", O PrimaryKey) val userName = column[String]("USER_NAME", O PrimaryKey)
val isManager = column[Boolean]("MANAGER") val manager = column[Boolean]("MANAGER")
def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply) def * = (groupName, userName, manager) <> (GroupMember.tupled, GroupMember.unapply)
} }
} }
case class GroupMember( case class GroupMember(
groupName: String, groupName: String,
userName: String, userName: String,
isManager: Boolean manager: Boolean
) )

View File

@@ -26,7 +26,7 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
case class Repository( case class Repository(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
isPrivate: Boolean, `private`: Boolean,
description: Option[String], description: Option[String],
defaultBranch: String, defaultBranch: String,
registeredDate: java.util.Date, registeredDate: java.util.Date,

View File

@@ -7,7 +7,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL") val url = column[String]("URL")
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply) val token = column[Option[String]]("TOKEN", O.Nullable)
def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
} }
@@ -16,7 +17,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
case class WebHook( case class WebHook(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
url: String url: String,
token: Option[String]
) )
object WebHook { object WebHook {

View File

@@ -1,55 +0,0 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{Account, AccessToken}
import gitbucket.core.util.StringUtil
import scala.util.Random
trait AccessTokenService {
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20)
Random.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
def tokenToHash(token: String): String = StringUtil.sha1(token)
/**
* @retuen (TokenId, Token)
*/
def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = {
var token: String = null
var hash: String = null
do{
token = makeAccessTokenString
hash = tokenToHash(token)
}while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run)
val newToken = AccessToken(
userName = userName,
note = note,
tokenHash = hash)
val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken
(tokenId, token)
}
def getAccountByAccessToken(token: String)(implicit s: Session): Option[Account] =
Accounts
.innerJoin(AccessTokens)
.filter{ case (ac, t) => (ac.userName === t.userName) && (t.tokenHash === tokenToHash(token).bind) && (ac.removed === false.bind) }
.map{ case (ac, t) => ac }
.firstOption
def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] =
AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list
def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit =
AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete
}
object AccessTokenService extends AccessTokenService

View File

@@ -0,0 +1,65 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{Account, AccessToken}
import gitbucket.core.util.StringUtil
import gitbucket.core.servlet.Database._
import io.getquill._
import scala.util.Random
trait AccessTokenService {
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20)
Random.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
def tokenToHash(token: String): String = StringUtil.sha1(token)
/**
* @retuen (TokenId, Token)
*/
def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = {
var token: String = null
var hash: String = null
do{
token = makeAccessTokenString
hash = tokenToHash(token)
} while (
db.run(quote { (hash: String) => query[AccessToken].filter(_.tokenHash == hash).nonEmpty })(hash).head
)
val newToken = AccessToken(
userName = userName,
note = note,
tokenHash = hash)
// TODO Remain Slick code
val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken
(tokenId, token)
}
def getAccountByAccessToken(token: String): Option[Account] =
db.run(quote { (tokenHash: String) =>
query[AccessToken].filter(_.tokenHash == tokenHash)
.join(query[Account]).on { (t, a) => t.userName == a.userName && a.registeredDate == false }
.map { case (t, a) => a }
})(tokenToHash(token)).headOption
def getAccessTokens(userName: String): List[AccessToken] =
db.run(quote { (userName: String) =>
query[AccessToken].filter(_.userName == userName).sortBy(_.accessTokenId)(Ord.desc)
})(userName)
def deleteAccessToken(userName: String, accessTokenId: Int): Unit =
db.run(quote { (userName: String, accessTokenId: Int) =>
query[AccessToken].filter { t => t.userName == userName && t.accessTokenId == accessTokenId }.delete
})(List((userName, accessTokenId)))
}
object AccessTokenService extends AccessTokenService

View File

@@ -1,20 +1,23 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{GroupMember, Account} import java.util.Date
import gitbucket.core.model.{GroupMember, Account, Collaborator, Repository}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.{StringUtil, LDAPUtil} import gitbucket.core.util.{StringUtil, LDAPUtil}
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
import profile.simple._ import profile.simple._
import StringUtil._ import StringUtil._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
// TODO Why is direct import required?
import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.servlet.Database._
import io.getquill._
trait AccountService { trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService]) private val logger = LoggerFactory.getLogger(classOf[AccountService])
def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] = def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){ if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password) ldapAuthentication(settings, userName, password)
} else { } else {
@@ -24,22 +27,21 @@ trait AccountService {
/** /**
* Authenticate by internal database. * Authenticate by internal database.
*/ */
private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = { private def defaultAuthentication(userName: String, password: String) = {
getAccountByUserName(userName).collect { getAccountByUserName(userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) case account if(!account.groupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None } getOrElse None
} }
/** /**
* Authenticate by LDAP. * Authenticate by LDAP.
*/ */
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) private def ldapAuthentication(settings: SystemSettings, userName: String, password: String): Option[Account] = {
(implicit s: Session): Option[Account] = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match { LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(ldapUserInfo) => { case Right(ldapUserInfo) => {
// Create or update account by LDAP information // Create or update account by LDAP information
getAccountByUserName(ldapUserInfo.userName, true) match { getAccountByUserName(ldapUserInfo.userName, true) match {
case Some(x) if(!x.isRemoved) => { case Some(x) if(!x.removed) => {
if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) { if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
updateAccount(x.copy(fullName = ldapUserInfo.fullName)) updateAccount(x.copy(fullName = ldapUserInfo.fullName))
} else { } else {
@@ -47,16 +49,16 @@ trait AccountService {
} }
getAccountByUserName(ldapUserInfo.userName) getAccountByUserName(ldapUserInfo.userName)
} }
case Some(x) if(x.isRemoved) => { case Some(x) if(x.removed) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.") logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
case Some(x) if(!x.isRemoved) => { case Some(x) if(!x.removed) => {
updateAccount(x.copy(fullName = ldapUserInfo.fullName)) updateAccount(x.copy(fullName = ldapUserInfo.fullName))
getAccountByUserName(ldapUserInfo.userName) getAccountByUserName(ldapUserInfo.userName)
} }
case Some(x) if(x.isRemoved) => { case Some(x) if(x.removed) => {
logger.info("LDAP Authentication Failed: Account is already registered but disabled.") logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
defaultAuthentication(userName, password) defaultAuthentication(userName, password)
} }
@@ -74,113 +76,163 @@ trait AccountService {
} }
} }
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByUserName(userName: String, includeRemoved: Boolean = false): Option[Account] = {
Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption db.run(quote { (userName: String, includeRemoved: Boolean) =>
query[Account].filter { t =>
if(includeRemoved){
t.userName == userName
} else {
t.userName == userName && t.removed == false
}
}
})(userName, includeRemoved).headOption
}
def getAccountsByUserNames(userNames: Set[String], knowns:Set[Account], includeRemoved: Boolean = false)(implicit s: Session): Map[String, Account] = {
def getAccountsByUserNames(userNames: Set[String], knowns:Set[Account], includeRemoved: Boolean = false): Map[String, Account] = {
val map = knowns.map(a => a.userName -> a).toMap val map = knowns.map(a => a.userName -> a).toMap
val needs = userNames -- map.keySet val needs = userNames -- map.keySet
if(needs.isEmpty){ if(needs.isEmpty){
map map
}else{ } else {
map ++ Accounts.filter(t => (t.userName inSetBind needs) && (t.removed === false.bind, !includeRemoved)).list.map(a => a.userName -> a).toMap map ++ db.run(quote { (userNames: Set[String]) =>
query[Account].filter { t => userNames.contains(t.userName) && t.removed == false }
})(userNames.toSet).map { a => a.userName -> a }.toMap
} }
} }
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false): Option[Account] = {
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption db.run(quote { (mailAddress: String, includeRemoved: Boolean) =>
query[Account].filter { t =>
if(includeRemoved){
t.mailAddress.toLowerCase == mailAddress.toLowerCase
} else {
t.mailAddress.toLowerCase == mailAddress.toLowerCase && t.removed == false
}
}
})(mailAddress, includeRemoved).headOption
}
def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] = def getAllUsers(includeRemoved: Boolean = true): List[Account] = {
if(includeRemoved){ db.run(
Accounts sortBy(_.userName) list if(includeRemoved){
} else { quote { query[Account].sortBy(_.userName) }
Accounts filter (_.removed === false.bind) sortBy(_.userName) list } else {
} quote { query[Account].filter(_.removed == false).sortBy(_.userName) }
}
)
}
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit = {
(implicit s: Session): Unit = db.run(quote { query[Account].insert })(Account(
Accounts insert Account(
userName = userName, userName = userName,
password = password, password = password,
fullName = fullName, fullName = fullName,
mailAddress = mailAddress, mailAddress = mailAddress,
isAdmin = isAdmin, administrator = isAdmin,
url = url, url = url,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None, lastLoginDate = None,
image = None, image = None,
isGroupAccount = false, groupAccount = false,
isRemoved = false) removed = false
))
}
def updateAccount(account: Account)(implicit s: Session): Unit = def updateAccount(account: Account): Unit = {
Accounts db.run(quote { (userName: String, password: String, fullName: String, mailAddress: String, administrator: Boolean,
.filter { a => a.userName === account.userName.bind } url: Option[String], registeredDate: Date, updatedDate: Date, lastLoginDate: Option[Date], removed: Boolean) =>
.map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) } query[Account].filter(_.userName == userName).update(
.update ( _.password -> password,
account.password, _.fullName -> fullName,
account.fullName, _.mailAddress -> mailAddress,
account.mailAddress, _.administrator -> administrator,
account.isAdmin, _.url -> url,
account.url, _.registeredDate -> registeredDate,
account.registeredDate, _.updatedDate -> updatedDate,
currentDate, _.lastLoginDate -> lastLoginDate,
account.lastLoginDate, _.removed -> removed
account.isRemoved) )
})((
account.userName,
account.password,
account.fullName,
account.mailAddress,
account.administrator,
account.url,
account.registeredDate,
currentDate,
account.lastLoginDate,
account.removed
))
}
def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit = def updateAvatarImage(userName: String, image: Option[String]): Unit = {
Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image) db.run(quote { (userName: String, image: Option[String]) =>
query[Account].filter(_.userName == userName).update(_.image -> image)
})((userName, image))
}
def updateLastLoginDate(userName: String)(implicit s: Session): Unit = def updateLastLoginDate(userName: String): Unit = {
Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate) db.run(quote { (userName: String, lastLoginDate: Option[Date]) =>
query[Account].filter(_.userName == userName).update(_.lastLoginDate -> lastLoginDate)
})((userName, Some(currentDate)))
}
def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit = def createGroup(groupName: String, url: Option[String]): Unit = {
Accounts insert Account( db.run( quote { query[Account].insert })(List(Account(
userName = groupName, userName = groupName,
password = "", password = "",
fullName = groupName, fullName = groupName,
mailAddress = groupName + "@devnull", mailAddress = groupName + "@devnull",
isAdmin = false, administrator = false,
url = url, url = url,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None, lastLoginDate = None,
image = None, image = None,
isGroupAccount = true, groupAccount = true,
isRemoved = false) removed = false
)))
}
def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit = def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = {
Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) db.run(quote { (groupName: String, url: Option[String], removed: Boolean) =>
query[Account].filter(_.userName == groupName).update(_.url -> url, _.removed -> removed)
})(List((groupName, url, removed)))
}
def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = {
db.run(quote { (groupName: String) => query[GroupMember].filter(_.groupName == groupName).delete })(groupName)
def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
GroupMembers.filter(_.groupName === groupName.bind).delete
members.foreach { case (userName, isManager) => members.foreach { case (userName, isManager) =>
GroupMembers insert GroupMember (groupName, userName, isManager) db.run(quote { query[GroupMember].insert })(GroupMember(groupName, userName, isManager))
} }
} }
def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] = def getGroupMembers(groupName: String): List[GroupMember] = {
GroupMembers db.run(quote { (groupName: String) =>
.filter(_.groupName === groupName.bind) query[GroupMember].filter(_.groupName == groupName).sortBy(_.userName)
.sortBy(_.userName) })(groupName)
.list
def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
GroupMembers
.filter(_.userName === userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
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] = { def getGroupsByUserName(userName: String): List[String] = {
List(userName) ++ db.run(quote { (userName: String) =>
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list query[GroupMember].filter(_.userName == userName).sortBy(_.groupName).map(_.groupName)
})(userName)
}
def removeUserRelatedData(userName: String): Unit = {
db.run(quote { (userName: String) => query[GroupMember].filter(_.userName == userName).delete })(userName)
db.run(quote { (userName: String) => query[Collaborator].filter(_.collaboratorName == userName).delete })(userName)
db.run(quote { (userName: String) => query[Repository].filter(_.userName == userName).delete })(userName)
}
def getGroupNames(userName: String): List[String] = {
List(userName) ++ db.run(quote { (userName: String) =>
query[Collaborator].filter(_.collaboratorName == userName).sortBy(_.userName).map(_.userName)
})(userName)
} }
} }

View File

@@ -1,194 +1,180 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.Activity import gitbucket.core.model._
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
import profile.simple._ import profile.simple._
import gitbucket.core.servlet.Database._
import io.getquill._
trait ActivityService { trait ActivityService {
def deleteOldActivities(limit: Int)(implicit s: Session): Int = { def deleteOldActivities(limit: Int): Int =
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id => db.run (quote { (limit: Int) =>
Activities.filter(_.activityId <= id.bind).delete query[Activity].map(_.activityId).sortBy(x => x)(Ord.desc).drop(limit)
})(limit).headOption.map { activityId =>
db.run (
quote { (activityId: Int) => query[Activity].filter(_.activityId <= activityId).delete }
)(activityId)
} getOrElse 0 } getOrElse 0
}
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
Activities db.run(quote { (activityUserName: String, isPublic: Boolean) =>
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) query[Activity].join(query[Repository]).on((a, r) => a.userName == r.userName && a.repositoryName == r.repositoryName)
.filter { case (t1, t2) => .filter { case (a, r) =>
if(isPublic){ if(isPublic){
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) a.activityUserName == activityUserName
} else { } else {
(t1.activityUserName === activityUserName.bind) a.activityUserName == activityUserName && r.`private` == false
}
} }
} .sortBy { case (a, r) => a.activityId }(Ord.desc)
.sortBy { case (t1, t2) => t1.activityId desc } .map { case (a, r) => a }
.map { case (t1, t2) => t1 } .take(30)
.take(30) })(activityUserName, isPublic)
.list
def getRecentActivities()(implicit s: Session): List[Activity] = def getRecentActivities(): List[Activity] =
Activities db.run(quote {
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) query[Activity].join(query[Repository]).on((a, r) => a.userName == r.userName && a.repositoryName == r.repositoryName)
.filter { case (t1, t2) => t2.isPrivate === false.bind } .filter { case (a, r) => r.`private` == false}
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (a, r) => a.activityId }(Ord.desc)
.map { case (t1, t2) => t1 } .map { case (a, r) => a }
.take(30) .take(30)
.list })
def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] = def getRecentActivitiesByOwners(owners : Set[String]): List[Activity] =
Activities db.run(quote { (owners: Set[String]) =>
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) query[Activity].join(query[Repository]).on((a, r) => a.userName == r.userName && a.repositoryName == r.repositoryName)
.filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) } .filter { case (a, r) => r.`private` == false || owners.contains(r.userName) }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (a, r) => a.activityId }(Ord.desc)
.map { case (t1, t2) => t1 } .map { case (a, r) => a }
.take(30) .take(30)
.list })(owners)
def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String) def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"create_repository", "create_repository",
s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
None, None)
currentDate)
def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"open_issue", "open_issue",
s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title))
currentDate)
def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title))
currentDate)
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"close_issue", "close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title))
currentDate)
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"reopen_issue", "reopen_issue",
s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title))
currentDate)
def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)))
currentDate)
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_issue", "comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)))
currentDate)
def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String) def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"comment_commit", "comment_commit",
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]", s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)))
currentDate
)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"create_wiki", "create_wiki",
s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
Some(pageName), Some(pageName))
currentDate)
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"edit_wiki", "edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName + ":" + commitId), Some(pageName + ":" + commitId))
currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
branchName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = branchName: String, commits: List[JGitUtil.CommitInfo]): Unit =
Activities insert Activity(userName, repositoryName, activityUserName, insertActivity(userName, repositoryName, activityUserName,
"push", "push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")))
currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = tagName: String, commits: List[JGitUtil.CommitInfo]): Unit =
Activities insert Activity(userName, repositoryName, activityUserName, insertActivity(userName, repositoryName, activityUserName,
"create_tag", "create_tag",
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None, None)
currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String, def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = tagName: String, commits: List[JGitUtil.CommitInfo]): Unit =
Activities insert Activity(userName, repositoryName, activityUserName, insertActivity(userName, repositoryName, activityUserName,
"delete_tag", "delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None, None)
currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"create_branch", "create_branch",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None, None)
currentDate)
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"delete_branch", "delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None, None)
currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit = def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String): Unit =
Activities insert Activity(userName, repositoryName, activityUserName, insertActivity(userName, repositoryName, activityUserName,
"fork", "fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
None, None)
currentDate)
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"open_pullreq", "open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title), Some(title))
currentDate)
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String) def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
(implicit s: Session): Unit = insertActivity(userName, repositoryName, activityUserName,
Activities insert Activity(userName, repositoryName, activityUserName,
"merge_pullreq", "merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message), Some(message))
currentDate)
private def insertActivity(userName: String, repositoryName: String, activityUserName: String, activityType: String,
message: String, additionalInfo: Option[String]): Unit = {
db.run(quote { query[Activity].insert })(Activity(
userName = userName,
repositoryName = repositoryName,
activityUserName = activityUserName,
activityType = activityType,
message = message,
additionalInfo = additionalInfo,
activityDate = currentDate
))
}
private def cut(value: String, length: Int): String = private def cut(value: String, length: Int): String =
if(value.length > length) value.substring(0, length) + "..." else value if(value.length > length) value.substring(0, length) + "..." else value

View File

@@ -1,35 +1,34 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.CommitComment import gitbucket.core.model.CommitComment
import gitbucket.core.util.{StringUtil, Implicits}
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import profile.simple._ import profile.simple._
import Implicits._
import StringUtil._ import gitbucket.core.servlet.Database._
import io.getquill._
trait CommitsService { trait CommitsService {
def getCommitComments(owner: String, repository: String, commitId: String, includePullRequest: Boolean)(implicit s: Session) = def getCommitComments(owner: String, repository: String, commitId: String, includePullRequest: Boolean) =
CommitComments filter { db.run(quote { (owner: String, repository: String, commitId: String, includePullRequest: Boolean) =>
t => t.byCommit(owner, repository, commitId) && (t.issueId.isEmpty || includePullRequest) query[CommitComment].filter { t =>
} list t.userName == owner && t.repositoryName == repository && t.commitId == commitId && (t.issueId.isEmpty || includePullRequest)
}
})(owner, repository, commitId, includePullRequest)
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) = def getCommitComment(owner: String, repository: String, commentId: String) =
if (commentId forall (_.isDigit)) if (commentId forall (_.isDigit))
CommitComments filter { t => db.run(quote { (owner: String, repository: String, commentId: Int) =>
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) query[CommitComment].filter(t => t.userName == owner && t.repositoryName == repository && t.commentId == commentId)
} firstOption })(owner, repository, commentId.toInt).headOption
else else
None None
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String, def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int],
issueId: Option[Int])(implicit s: Session): Int = issueId: Option[Int])(implicit s: Session): Int =
CommitComments.autoInc insert CommitComment( CommitComments.autoInc insert CommitComment( // TODO Remain Slick code
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
commitId = commitId, commitId = commitId,
@@ -42,13 +41,12 @@ trait CommitsService {
updatedDate = currentDate, updatedDate = currentDate,
issueId = issueId) issueId = issueId)
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) = def updateCommitComment(commentId: Int, content: String) =
CommitComments db.run(quote { (commentId: Int, content: String, updatedDate: java.util.Date) =>
.filter (_.byPrimaryKey(commentId)) query[CommitComment].filter(_.commentId == commentId).update(_.content -> content, _.updatedDate -> updatedDate)
.map { t => })(commentId, content, currentDate)
t.content -> t.updatedDate
}.update (content, currentDate) def deleteCommitComment(commentId: Int) =
db.run(quote { (commentId: Int) => query[CommitComment].filter(_.commentId == commentId).delete })(commentId)
def deleteCommitComment(commentId: Int)(implicit s: Session) =
CommitComments filter (_.byPrimaryKey(commentId)) delete
} }

View File

@@ -0,0 +1,91 @@
package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
import gitbucket.core.model.Profile._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Notifier
import profile.simple._
trait HandleCommentService {
self: RepositoryService with IssuesService with ActivityService
with WebHookService with WebHookIssueCommentService with WebHookPullRequestService =>
/**
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
(implicit context: Context, s: Session) = {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
val (action, recordActivity) = actionOpt
.collect {
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" if(issue.closed) => false ->
(Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issue.issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = (content, action) match {
case (None, None) => None
case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
}
// record comment activity if comment is entered
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issue.issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
// extract references and create refer comment
content.map { content =>
createReferComment(owner, name, issue, content, context.loginAccount.get)
}
// 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){
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
}
}
// notifications
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issue, _){
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
}
}
action foreach {
f.toNotify(repository, issue, _){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
}
}
}
commentId.map( issue -> _ )
}
}
}

View File

@@ -1,6 +1,8 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.StringUtil
import profile.simple._ import profile.simple._
import gitbucket.core.util.StringUtil._ import gitbucket.core.util.StringUtil._
@@ -12,6 +14,7 @@ import Q.interpolation
trait IssuesService { trait IssuesService {
self: AccountService =>
import IssuesService._ import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
@@ -394,12 +397,35 @@ trait IssuesService {
} }
} }
} }
def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = {
StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title
if(getIssue(owner, repository, issueId).isDefined){
// Not add if refer comment already exist.
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
createComment(owner, repository, loginAccount.userName, issueId.toInt, content, "refer")
}
}
}
}
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
} }
object IssuesService { object IssuesService {
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
val IssueLimit = 30 val IssueLimit = 25
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,

View File

@@ -76,7 +76,7 @@ object ProtectedBranchService {
includeAdministrators: Boolean) extends AccountService with CommitStatusService { includeAdministrators: Boolean) extends AccountService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean = def isAdministrator(pusher: String)(implicit session: Session): Boolean =
pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.manager).nonEmpty
/** /**
* Can't be force pushed * Can't be force pushed

View File

@@ -0,0 +1,79 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil
import gitbucket.core.model.Account
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{FileMode, Constants}
import profile.simple._
trait RepositoryCreationService {
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
(implicit s: Session) {
val ownerAccount = getAccountByUserName(owner).get
val loginUserName = loginAccount.userName
// Insert to the database at first
insertRepository(name, owner, description, isPrivate)
// Add collaborators for group repository
if(ownerAccount.groupAccount){
getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
if(createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(description.nonEmpty){
name + "\n" +
"===============\n" +
"\n" +
description.get
} else {
name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, owner, name)
// Record activity
recordCreateRepositoryActivity(owner, name, loginUserName)
}
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
}

View File

@@ -1,5 +1,6 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Collaborator, Repository, Account} import gitbucket.core.model.{Collaborator, Repository, Account}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
@@ -18,7 +19,7 @@ trait RepositoryService { self: AccountService =>
* @param originRepositoryName specify for the forked repository. (default is None) * @param originRepositoryName specify for the forked repository. (default is None)
* @param originUserName specify for the forked repository. (default is None) * @param originUserName specify for the forked repository. (default is None)
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None, originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
(implicit s: Session): Unit = { (implicit s: Session): Unit = {
@@ -26,7 +27,7 @@ trait RepositoryService { self: AccountService =>
Repository( Repository(
userName = userName, userName = userName,
repositoryName = repositoryName, repositoryName = repositoryName,
isPrivate = isPrivate, `private` = isPrivate,
description = description, description = description,
defaultBranch = "master", defaultBranch = "master",
registeredDate = currentDate, registeredDate = currentDate,
@@ -114,7 +115,7 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName repositoryName = newRepositoryName
)) :_*) )) :_*)
if(account.isGroupAccount){ if(account.groupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else { } else {
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
@@ -194,10 +195,9 @@ trait RepositoryService { self: AccountService =>
* *
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param repositoryName the repository name * @param repositoryName the repository name
* @param baseUrl the base url of this application
* @return the repository information * @return the repository information
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String)(implicit s: Session): Option[RepositoryInfo] = {
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count // for getting issue count and pull request count
val issues = Issues.filter { t => val issues = Issues.filter { t =>
@@ -205,7 +205,7 @@ trait RepositoryService { self: AccountService =>
}.map(_.pullRequest).list }.map(_.pullRequest).list
new RepositoryInfo( new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
repository, repository,
issues.count(_ == false), issues.count(_ == false),
issues.count(_ == true), issues.count(_ == true),
@@ -234,7 +234,7 @@ trait RepositoryService { self: AccountService =>
}.list }.list
} }
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = { (implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 => Repositories.filter { t1 =>
(t1.userName === userName.bind) || (t1.userName === userName.bind) ||
@@ -242,9 +242,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else { } else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
}, },
repository, repository,
getForkedCount( getForkedCount(
@@ -260,20 +260,19 @@ trait RepositoryService { self: AccountService =>
* If repositoryUserName is given then filters results by repository owner. * If repositoryUserName is given then filters results by repository owner.
* *
* @param loginAccount the logged in account * @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags * branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate. * @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, def getVisibleRepositories(loginAccount: Option[Account], repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false) withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = { (implicit s: Session): List[RepositoryInfo] = {
(loginAccount match { (loginAccount match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Repositories case Some(x) if(x.administrator) => Repositories
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.administrator) =>
Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || 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) (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
} }
@@ -284,9 +283,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository => }.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo( new RepositoryInfo(
if(withoutPhysicalInfo){ if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else { } else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
}, },
repository, repository,
getForkedCount( getForkedCount(
@@ -298,8 +297,8 @@ trait RepositoryService { self: AccountService =>
} }
private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] = private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
if(getAccountByUserName(userName).exists(_.isGroupAccount)){ if(getAccountByUserName(userName).exists(_.groupAccount)){
getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } getGroupMembers(userName).collect { case x if(x.manager) => x.userName }
} else { } else {
Seq(userName) Seq(userName)
} }
@@ -366,7 +365,7 @@ trait RepositoryService { self: AccountService =>
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.administrator) => true
case Some(a) if(a.userName == owner) => true case Some(a) if(a.userName == owner) => true
case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true
case _ => false case _ => false
@@ -389,32 +388,39 @@ trait RepositoryService { self: AccountService =>
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){ branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
/** /**
* Creates instance with issue count and pull request count. * Creates instance with issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(
repo.owner, repo.name, model,
issueCount, pullCount, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
/** /**
* Creates instance without issue count and pull request count. * Creates instance without issue count and pull request count.
*/ */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) this(
repo.owner, repo.name, model,
0, 0, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
if(context.settings.ssh){
context.loginAccount.flatMap { loginAccount =>
context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" }
}
} else None
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
} }

View File

@@ -1,6 +1,7 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.util.{Directory, ControlUtil} import gitbucket.core.util.{Directory, ControlUtil}
import gitbucket.core.util.Implicits._
import Directory._ import Directory._
import ControlUtil._ import ControlUtil._
import SystemSettingsService._ import SystemSettingsService._
@@ -21,6 +22,7 @@ trait SystemSettingsService {
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString) props.setProperty(Ssh, settings.ssh.toString)
settings.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
props.setProperty(UseSMTP, settings.useSMTP.toString) props.setProperty(UseSMTP, settings.useSMTP.toString)
if(settings.useSMTP) { if(settings.useSMTP) {
@@ -75,6 +77,7 @@ trait SystemSettingsService {
getValue(props, Notification, false), getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None), getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false), getValue(props, Ssh, false),
getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort)), getOptionValue(props, SshPort, Some(DefaultSshPort)),
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
if(getValue(props, UseSMTP, getValue(props, Notification, false))){ if(getValue(props, UseSMTP, getValue(props, Notification, false))){
@@ -126,16 +129,19 @@ object SystemSettingsService {
notification: Boolean, notification: Boolean,
activityLogLimit: Option[Int], activityLogLimit: Option[Int],
ssh: Boolean, ssh: Boolean,
sshHost: Option[String],
sshPort: Option[Int], sshPort: Option[Int],
useSMTP: Boolean, useSMTP: Boolean,
smtp: Option[Smtp], smtp: Option[Smtp],
ldapAuthentication: Boolean, ldapAuthentication: Boolean,
ldap: Option[Ldap]){ ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
} }
}.stripSuffix("/") yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
} }
case class Ldap( case class Ldap(
@@ -161,6 +167,10 @@ object SystemSettingsService {
fromAddress: Option[String], fromAddress: Option[String],
fromName: Option[String]) fromName: Option[String])
case class SshAddress(
host:String,
port:Int)
val DefaultSshPort = 29418 val DefaultSshPort = 29418
val DefaultSmtpPort = 25 val DefaultSmtpPort = 25
val DefaultLdapPort = 389 val DefaultLdapPort = 389
@@ -174,6 +184,7 @@ object SystemSettingsService {
private val Notification = "notification" private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit" private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh" private val Ssh = "ssh"
private val SshHost = "ssh.host"
private val SshPort = "ssh.port" private val SshPort = "ssh.port"
private val UseSMTP = "useSMTP" private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host" private val SmtpHost = "smtp.host"
@@ -216,7 +227,4 @@ object SystemSettingsService {
else value else value
} }
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
} }

View File

@@ -1,8 +1,13 @@
package gitbucket.core.service package gitbucket.core.service
import java.io.ByteArrayInputStream
import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import org.apache.http.client.utils.URLEncodedUtils
import profile.simple._ import profile.simple._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName import gitbucket.core.util.RepositoryName
@@ -33,8 +38,11 @@ trait WebHookService {
/** get All WebHook informations of repository event */ /** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind) WebHooks.filter(_.byRepository(owner, repository))
.list.map(t => WebHook(t.userName, t.repositoryName, t.url)) .innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
.filter{ case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh }
.list.distinct
/** get All WebHook information from repository to url */ /** get All WebHook information from repository to url */
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
@@ -44,14 +52,15 @@ trait WebHookService {
.map{ case (w,t) => w -> t.event } .map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url) WebHooks insert WebHook(owner, repository, url, token)
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) WebHookEvents insert WebHookEvent(owner, repository, url, event)
} }
} }
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token)
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
events.toSet.map{ event: WebHook.Event => events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) WebHookEvents insert WebHookEvent(owner, repository, url, event)
@@ -69,17 +78,17 @@ trait WebHookService {
} }
} }
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload) def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = { (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClientBuilder
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.http.protocol.HttpContext import org.apache.http.protocol.HttpContext
import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPost
if(webHookURLs.nonEmpty){ if(webHooks.nonEmpty){
val json = JsonFormat(payload) val json = JsonFormat(payload)
webHookURLs.map { webHookUrl => webHooks.map { webHook =>
val reqPromise = Promise[HttpRequest] val reqPromise = Promise[HttpRequest]
val f = Future { val f = Future {
val itcp = new org.apache.http.HttpRequestInterceptor{ val itcp = new org.apache.http.HttpRequestInterceptor{
@@ -89,19 +98,26 @@ trait WebHookService {
} }
try{ try{
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHookUrl.url}") logger.debug(s"start web hook invocation for ${webHook.url}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHook.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Event", event.name)
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
val params: java.util.List[NameValuePair] = new java.util.ArrayList() val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json)) params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) def postContent = new UrlEncodedFormEntity(params, "UTF-8")
httpPost.setEntity(postContent)
if (!webHook.token.isEmpty) {
// TODO find a better way and see how to extract content from postContent
val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8")
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, contentAsBytes))
}
val res = httpClient.execute(httpPost) val res = httpClient.execute(httpPost)
httpPost.releaseConnection() httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}") logger.debug(s"end web hook invocation for ${webHook}")
res res
}catch{ }catch{
case e:Throwable => { case e:Throwable => {
@@ -113,12 +129,12 @@ trait WebHookService {
} }
} }
f.onSuccess { f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") case s => logger.debug(s"Success: web hook request to ${webHook.url}")
} }
f.onFailure { f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) case t => logger.error(s"Failed: web hook request to ${webHook.url}", t)
} }
(webHookUrl, json, reqPromise.future, f) (webHook, json, reqPromise.future, f)
} }
} else { } else {
Nil Nil
@@ -161,7 +177,7 @@ trait WebHookPullRequestService extends WebHookService {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestPayload( WebHookPullRequestPayload(
action = action, action = action,
@@ -200,7 +216,7 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._ import WebHookService._
for{ for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch) ((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl) baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield { } yield {
val payload = WebHookPullRequestPayload( val payload = WebHookPullRequestPayload(
action = action, action = action,
@@ -229,7 +245,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestReviewCommentPayload( WebHookPullRequestReviewCommentPayload(
action = action, action = action,

View File

@@ -1,7 +1,9 @@
package gitbucket.core.service package gitbucket.core.service
import java.util.Date import java.util.Date
import gitbucket.core.controller.Context
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -13,7 +15,6 @@ import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._ import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import RepositoryService.RepositoryInfo
object WikiService { object WikiService {
@@ -38,10 +39,13 @@ object WikiService {
*/ */
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") = RepositoryService.httpUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
def wikiSshUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): Option[String]
= RepositoryService.sshUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
} }
trait WikiService { trait WikiService {

View File

@@ -21,6 +21,8 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current GitBucket version. * The history of versions. A head of this sequence is the current GitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 13),
new Version(3, 12),
new Version(3, 11), new Version(3, 11),
new Version(3, 10), new Version(3, 10),
new Version(3, 9), new Version(3, 9),

View File

@@ -74,16 +74,16 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
request.paths match { request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) => case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
case Some(repository) => { case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ if(!isUpdating && !repository.repository.`private` && settings.allowAnonymousAccess){
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
val passed = for { val passed = for {
auth <- Option(request.getHeader("Authorization")) auth <- Option(request.getHeader("Authorization"))
Array(username, password) = decodeAuthHeader(auth).split(":", 2) Array(username, password) = decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password) account <- authenticate(settings, username, password)
} yield if(isUpdating || repository.repository.isPrivate){ } yield if(isUpdating || repository.repository.`private`){
if(hasWritePermission(repository.owner, repository.name, Some(account))){ if(hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName) request.setAttribute(Keys.Request.UserName, account.userName)
true true

View File

@@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util._ import gitbucket.core.util._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -160,7 +159,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
val repositoryInfo = getRepository(owner, repository, baseUrl).get val repositoryInfo = getRepository(owner, repository).get
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = repositoryInfo.repository.defaultBranch val defaultBranch = repositoryInfo.repository.defaultBranch
@@ -168,7 +167,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) { if (issueCount > 0) {
pushedIds.add(commit.id) pushedIds.add(commit.id)
createIssueComment(commit) createIssueComment(owner, repository, commit)
// close issues // close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository) closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
@@ -230,13 +229,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
} }
private def createIssueComment(commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
} }

View File

@@ -4,10 +4,14 @@ import javax.servlet._
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import com.mchange.v2.c3p0.ComboPooledDataSource import com.mchange.v2.c3p0.ComboPooledDataSource
import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.DatabaseConfig
import io.getquill._
import io.getquill.naming.SnakeCase
import io.getquill.sources.sql.idiom.H2Dialect
import org.scalatra.ScalatraBase import org.scalatra.ScalatraBase
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session} import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session}
import gitbucket.core.util.Keys import gitbucket.core.util.Keys
import Database._
/** /**
* Controls the transaction with the open session in view pattern. * Controls the transaction with the open session in view pattern.
@@ -25,17 +29,20 @@ class TransactionFilter extends Filter {
// assets don't need transaction // assets don't need transaction
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
Database() withTransaction { session => db.transaction {
// Register Scalatra error callback to rollback transaction // TODO Delete after moving to quill
ScalatraBase.onFailure { _ => Database() withTransaction { session =>
logger.debug("Rolled back transaction") // Register Scalatra error callback to rollback transaction
session.rollback() ScalatraBase.onFailure { _ =>
}(req.asInstanceOf[HttpServletRequest]) logger.debug("Rolled back transaction")
session.rollback()
}(req.asInstanceOf[HttpServletRequest])
logger.debug("begin transaction") logger.debug("begin transaction")
req.setAttribute(Keys.Request.DBSession, session) req.setAttribute(Keys.Request.DBSession, session)
chain.doFilter(req, res) chain.doFilter(req, res)
logger.debug("end transaction") logger.debug("end transaction")
}
} }
} }
} }
@@ -46,6 +53,9 @@ object Database {
private val logger = LoggerFactory.getLogger(Database.getClass) private val logger = LoggerFactory.getLogger(Database.getClass)
lazy val db = source(new JdbcSourceConfig[H2Dialect, SnakeCase]("db"))
// TODO Delete after moving to quill
private val dataSource: ComboPooledDataSource = { private val dataSource: ComboPooledDataSource = {
val ds = new ComboPooledDataSource val ds = new ComboPooledDataSource
ds.setDriverClass(DatabaseConfig.driver) ds.setDriverClass(DatabaseConfig.driver)
@@ -56,15 +66,19 @@ object Database {
ds ds
} }
private val db: SlickDatabase = { // TODO Delete after moving to quill
private val slickDatabase: SlickDatabase = {
SlickDatabase.forDataSource(dataSource) SlickDatabase.forDataSource(dataSource)
} }
def apply(): SlickDatabase = db // TODO Delete after moving to quill
def apply(): SlickDatabase = slickDatabase
// TODO Delete after moving to quill
def getSession(req: ServletRequest): Session = def getSession(req: ServletRequest): Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session] req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session]
// TODO Delete after moving to quill
def closeDataSource(): Unit = dataSource.close def closeDataSource(): Unit = dataSource.close
} }

View File

@@ -87,12 +87,12 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
} }
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName) class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService { with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ if(!repositoryInfo.repository.`private` || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository val repository = git.getRepository
val upload = new UploadPack(repository) val upload = new UploadPack(repository)
@@ -107,7 +107,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
with RepositoryService with AccountService { with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){ if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git => using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository val repository = git.getRepository
@@ -124,7 +124,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
} }
} }
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService { with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -139,7 +139,7 @@ class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitReposit
} }
} }
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService { with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = { override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -163,9 +163,9 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
logger.debug(s"command: $command") logger.debug(s"command: $command")
command match { command match {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName)) case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName)) case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl) case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl) case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case _ => new UnknownCommand(command) case _ => new UnknownCommand(command)
} }

View File

@@ -1,12 +1,13 @@
package gitbucket.core.ssh package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.common.Factory import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command} import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream} import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
class NoShell extends Factory[Command] with SystemSettingsService { class NoShell(sshAddress:SshAddress) extends Factory[Command] {
override def create(): Command = new Command() { override def create(): Command = new Command() {
private var in: InputStream = null private var in: InputStream = null
private var out: OutputStream = null private var out: OutputStream = null
@@ -15,7 +16,6 @@ class NoShell extends Factory[Command] with SystemSettingsService {
override def start(env: Environment): Unit = { override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER") val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message = val message =
""" """
| Welcome to | Welcome to
@@ -31,8 +31,8 @@ class NoShell extends Factory[Command] with SystemSettingsService {
| |
| Please use: | Please use:
| |
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" """.stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message)) err.write(Constants.encode(message))
err.flush() err.flush()
in.close() in.close()

View File

@@ -5,7 +5,8 @@ import java.util.concurrent.atomic.AtomicBoolean
import javax.servlet.{ServletContextEvent, ServletContextListener} import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.Directory import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.{Directory}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -14,20 +15,20 @@ object SshServer {
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer() private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false) private val active = new AtomicBoolean(false)
private def configure(port: Int, baseUrl: String) = { private def configure(sshAddress: SshAddress, baseUrl: String) = {
server.setPort(port) server.setPort(sshAddress.port)
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser")) val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
provider.setAlgorithm("RSA") provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false) provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider) server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator) server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl)) server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell) server.setShellFactory(new NoShell(sshAddress))
} }
def start(port: Int, baseUrl: String) = { def start(sshAddress: SshAddress, baseUrl: String) = {
if(active.compareAndSet(false, true)){ if(active.compareAndSet(false, true)){
configure(port, baseUrl) configure(sshAddress, baseUrl)
server.start() server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}") logger.info(s"Start SSH Server Listen on ${server.getPort}")
} }
@@ -55,20 +56,18 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
override def contextInitialized(sce: ServletContextEvent): Unit = { override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings() val settings = loadSystemSettings()
if(settings.ssh){ if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
settings.baseUrl match { logger.error("Could not start SshServer because the baseUrl is not configured.")
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
} }
for {
sshAddress <- settings.sshAddress
baseUrl <- settings.baseUrl
}
SshServer.start(sshAddress, baseUrl)
} }
override def contextDestroyed(sce: ServletContextEvent): Unit = { override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){ SshServer.stop()
SshServer.stop()
}
} }
} }

View File

@@ -17,7 +17,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action case Some(x) if(x.administrator) => action
case Some(x) if(paths(0) == x.userName) => action case Some(x) if(paths(0) == x.userName) => action
case _ => Unauthorized() case _ => Unauthorized()
} }
@@ -36,12 +36,12 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.administrator) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member => case Some(x) if(getGroupMembers(repository.owner).exists { member =>
member.userName == x.userName && member.isManager == true member.userName == x.userName && member.manager == true
}) => action(repository) }) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
@@ -78,7 +78,7 @@ trait AdminAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = { private def authenticate(action: => Any) = {
{ {
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action case Some(x) if(x.administrator) => action
case _ => Unauthorized() case _ => Unauthorized()
} }
} }
@@ -95,9 +95,9 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.administrator) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
@@ -118,12 +118,12 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
if(!repository.repository.isPrivate){ if(!repository.repository.`private`){
action(repository) action(repository)
} else { } else {
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.administrator) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
@@ -145,10 +145,10 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
defining(request.paths){ paths => defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository => getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.administrator) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository) case Some(x) if(!repository.repository.`private`) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
@@ -171,7 +171,7 @@ trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
defining(request.paths){ paths => defining(request.paths){ paths =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(getGroupMembers(paths(0)).exists { member => case Some(x) if(getGroupMembers(paths(0)).exists { member =>
member.userName == x.userName && member.isManager member.userName == x.userName && member.manager
}) => action }) => action
case _ => Unauthorized() case _ => Unauthorized()
} }

View File

@@ -75,6 +75,11 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/") def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
def baseUrl:String = {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
}
} }
implicit class RichSession(session: HttpSession){ implicit class RichSession(session: HttpSession){

View File

@@ -32,14 +32,13 @@ object JGitUtil {
* *
* @param owner the user name of the repository owner * @param owner the user name of the repository owner
* @param name the repository name * @param name the repository name
* @param url the repository URL
* @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001.
* @param branchList the list of branch names * @param branchList the list of branch names
* @param tags the list of tags * @param tags the list of tags
*/ */
case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
def this(owner: String, name: String, baseUrl: String) = { def this(owner: String, name: String) = {
this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil) this(owner, name, 0, Nil, Nil)
} }
} }
@@ -174,14 +173,14 @@ object JGitUtil {
/** /**
* Returns the repository information. It contains branch names and tag names. * Returns the repository information. It contains branch names and tag names.
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = {
using(Git.open(getRepositoryDir(owner, repository))){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository,
// commit count // commit count
commitCount, commitCount,
// branches // branches
@@ -197,7 +196,7 @@ object JGitUtil {
} catch { } catch {
// not initialized // not initialized
case e: NoHeadException => RepositoryInfo( case e: NoHeadException => RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil) owner, repository, 0, Nil, Nil)
} }
} }

View File

@@ -30,7 +30,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
) )
.distinct .distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) ) .foreach ( getAccountByUserName(_) filterNot (_.groupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
} }

View File

@@ -9,7 +9,7 @@ import gitbucket.core.plugin.{RenderRequest, PluginRegistry}
import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil} import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
import play.twirl.api.Html import play.twirl.api.{Html, HtmlFormat}
/** /**
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
@@ -225,6 +225,13 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress)) userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
/**
* Generates the avatar link to the account page.
* If user does not exist or disabled, this method returns avatar image without link.
*/
def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html = private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
(if(mailAddress.isEmpty){ (if(mailAddress.isEmpty){
getAccountByUserName(userName) getAccountByUserName(userName)
@@ -306,6 +313,19 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String): Html = { def detectAndRenderLinks(text: String): Html = {
Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""<a href="${m.group(0)}">${m.group(0)}</a>""")) val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
val url = m.group(0)
val href = url.replace("\"", "&quot;")
(x ++ (Seq(
if(pos < m.start) Some(HtmlFormat.escape(text.substring(pos, m.start))) else None,
Some(Html(s"""<a href="${href}">${url}</a>"""))
).flatten), m.end)
}
// append rest fragment
val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x
HtmlFormat.fill(out)
} }
} }

View File

@@ -4,7 +4,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Applications"){ @html.main("Applications"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("application", settings.ssh) @menu("application", settings.ssh)
@@ -16,24 +16,27 @@
@if(personalTokens.isEmpty && gneratedToken.isEmpty){ @if(personalTokens.isEmpty && gneratedToken.isEmpty){
No tokens. No tokens.
} else { } else {
Tokens you have generated that can be used to access the GitBucket API.<hr> Tokens you have generated that can be used to access the GitBucket API.
<hr style="margin-top: 10px;">
} }
@gneratedToken.map{ case (token, tokenString) => @gneratedToken.map{ case (token, tokenString) =>
<div class="alert alert-info"> <div class="alert alert-info">
Make sure to copy your new personal access token now. You won't be able to see it again! Make sure to copy your new personal access token now. You won't be able to see it again!
</div> </div>
@helper.html.copy("generated-token-copy", tokenString){ <a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
<input type="text" value="@tokenString" style="width:21em" readonly> <div style="width: 50%;">
} @helper.html.copy("generated-token-copy", tokenString){
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a> <input type="text" value="@tokenString" class="form-control input-sm" readonly>
<hr> }
</div>
<hr style="margin-top: 10px;">
} }
@personalTokens.zipWithIndex.map { case (token, i) => @personalTokens.zipWithIndex.map { case (token, i) =>
@if(i != 0){ @if(i != 0){
<hr> <hr style="margin-top: 10px;">
} }
<strong>@token.note</strong> <strong>@token.note</strong>
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-mini btn-danger pull-right">Delete</a> <a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
@@ -44,7 +47,7 @@
<fieldset> <fieldset>
<label for="note" class="strong">Token description</label> <label for="note" class="strong">Token description</label>
<div><span id="error-note" class="error"></span></div> <div><span id="error-note" class="error"></span></div>
<input type="text" name="note" id="note" class="form-control" style="width: 400px;"/> <input type="text" name="note" id="note" class="form-control"/>
<p class="muted">What's this token for?</p> <p class="muted">What's this token for?</p>
</fieldset> </fieldset>
<input type="submit" class="btn btn-success" value="Generate token"/> <input type="submit" class="btn btn-success" value="Generate token"/>

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Edit your profile"){ @html.main("Edit your profile"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("profile", settings.ssh) @menu("profile", settings.ssh)

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main(if(account.isEmpty) "Create group" else "Edit group"){ @html.main(if(account.isEmpty) "Create group" else "Edit group"){
<div class="container"> <div class="container body">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-5">
@@ -32,7 +32,7 @@
</ul> </ul>
@helper.html.account("memberName", 200) @helper.html.account("memberName", 200)
<input type="button" class="btn btn-default" value="Add" id="addMember"/> <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(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.manager).mkString(",")"/>
<div> <div>
<span class="error" id="error-members"></span> <span class="error" id="error-members"></span>
</div> </div>
@@ -103,7 +103,7 @@ $(function(){
}); });
@members.map { member => @members.map { member =>
addMemberHTML('@member.userName', @member.isManager); addMemberHTML('@member.userName', @member.manager);
} }
function addMemberHTML(userName, isManager){ function addMemberHTML(userName, isManager){

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main(account.userName){ @html.main(account.userName){
<div class="container"> <div class="container body">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
@@ -22,7 +22,7 @@
<div> <div>
<div>Groups</div> <div>Groups</div>
@groupNames.map { groupName => @groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a> @avatarLink(groupName, 36, tooltip = true)
} }
</div> </div>
} }
@@ -31,7 +31,7 @@
<div class="col-md-8"> <div class="col-md-8">
<ul class="nav nav-tabs" style="margin-bottom: 5px;"> <ul class="nav nav-tabs" style="margin-bottom: 5px;">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li> <li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){ @if(account.groupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li> <li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else { } else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li> <li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@@ -43,9 +43,9 @@
</div> </div>
</li> </li>
} }
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){ @if(loginAccount.isDefined && account.groupAccount && isGroupManager){
<li class="pull-right"> <li class="pull-right">
<div class="button-group"> <div class="button-groRepiosup">
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a> <a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
</div> </div>
</li> </li>

View File

@@ -3,7 +3,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Create a New Repository"){ @html.main("Create a New Repository"){
<div style="width: 600px; margin: 10px auto;"> <div class="body" style="width: 600px; margin: 10px auto;">
<h2>Create a new repository</h2> <h2>Create a new repository</h2>
<p class="muted"> <p class="muted">
A repository contains all the files for your project, including the revision history. A repository contains all the files for your project, including the revision history.

View File

@@ -2,7 +2,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Create your account"){ @html.main("Create your account"){
<div class="container"> <div class="container body">
<h3>Create your account</h3> <h3>Create your account</h3>
<form action="@path/register" method="POST" validate="true"> <form action="@path/register" method="POST" validate="true">
<div class="row"> <div class="row">

View File

@@ -15,7 +15,7 @@
<div class="repository-content"> <div class="repository-content">
<div class="block-header"> <div class="block-header">
<a href="@url(repository)">@repository.name</a> <a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){ @if(repository.repository.`private`){
<i class="octicon octicon-lock"></i> <i class="octicon octicon-lock"></i>
} }
</div> </div>

View File

@@ -3,7 +3,7 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("SSH Keys"){ @html.main("SSH Keys"){
<div class="container"> <div class="container body">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
@menu("ssh", settings.ssh) @menu("ssh", settings.ssh)
@@ -17,10 +17,10 @@
} }
@sshKeys.zipWithIndex.map { case (key, i) => @sshKeys.zipWithIndex.map { case (key, i) =>
@if(i != 0){ @if(i != 0){
<hr> <hr style="margin-top: 10px;">
} }
<strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) <strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-mini btn-danger pull-right">Delete</a> <a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
@@ -31,12 +31,12 @@
<fieldset class="form-group"> <fieldset class="form-group">
<label for="title" class="strong">Title</label> <label for="title" class="strong">Title</label>
<div><span id="error-title" class="error"></span></div> <div><span id="error-title" class="error"></span></div>
<input type="text" name="title" id="title" class="form-control" style="width: 400px;"/> <input type="text" name="title" id="title" class="form-control"/>
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">
<label for="publicKey" class="strong">Key</label> <label for="publicKey" class="strong">Key</label>
<div><span id="error-publicKey" class="error"></span></div> <div><span id="error-publicKey" class="error"></span></div>
<textarea name="publicKey" id="publicKey" class="form-control" style="width: 600px; height: 250px;"></textarea> <textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
</fieldset> </fieldset>
<input type="submit" class="btn btn-success" value="Add"/> <input type="submit" class="btn btn-success" value="Add"/>
</div> </div>

View File

@@ -11,7 +11,7 @@
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li@if(active=="plugins"){ class="active"}> <li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a> <a href="@path/admin/plugins">Plugins</a>
</li> </li>
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp">H2 Console</a>

View File

@@ -111,15 +111,24 @@
Enable SSH access to git repository Enable SSH access to git repository
</label> </label>
</fieldset> </fieldset>
<div class="form-group ssh"> <div class="ssh">
<label class="control-label col-md-3" for="sshPort">SSH Port</label> <div class="form-group">
<div class="col-md-9"> <label class="control-label col-md-3" for="sshHost">SSH Host</label>
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/> <div class="col-md-9">
<span id="error-sshPort" class="error"></span> <input type="text" id="sshHost" name="sshHost" class="form-control" value="@settings.sshHost"/>
<span id="error-sshHost" class="error"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="sshPort">SSH Port</label>
<div class="col-md-9">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/>
<span id="error-sshPort" class="error"></span>
</div>
</div> </div>
</div> </div>
<p class="muted"> <p class="muted">
Base URL is required if SSH access is enabled. Both of SSH host and Base URL are required if SSH access is enabled.
</p> </p>
<!--====================================================================--> <!--====================================================================-->
<!-- Authentication --> <!-- Authentication -->

View File

@@ -13,7 +13,7 @@
<input type="text" name="userName" id="userName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <input type="text" name="userName" id="userName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){ @if(account.isDefined){
<label for="removed"> <label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/> <input type="checkbox" name="removed" id="removed" value="true" @if(account.get.removed){checked}/>
Disable Disable
</label> </label>
<div> <div>
@@ -53,10 +53,10 @@
<fieldset class="form-group"> <fieldset class="form-group">
<label class="strong">User Type:</label> <label class="strong">User Type:</label>
<label class="radio" for="userType_Normal"> <label class="radio" for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal <input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.administrator){ checked}/> Normal
</label> </label>
<label class="radio" for="userType_Admin"> <label class="radio" for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator <input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.administrator){ checked}/> Administrator
</label> </label>
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">

View File

@@ -14,7 +14,7 @@
<input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/> <input type="text" name="groupName" id="groupName" class="form-control" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
@if(account.isDefined){ @if(account.isDefined){
<label for="removed"> <label for="removed">
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/> <input type="checkbox" name="removed" id="removed" value="true" @if(account.get.removed){checked}/>
Disable Disable
</label> </label>
} }
@@ -38,7 +38,7 @@
</ul> </ul>
@helper.html.account("memberName", 200) @helper.html.account("memberName", 200)
<input type="button" class="btn btn-default" value="Add" id="addMember"/> <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(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.manager).mkString(",")"/>
<div> <div>
<span class="error" id="error-members"></span> <span class="error" id="error-members"></span>
</div> </div>
@@ -98,7 +98,7 @@ $(function(){
}); });
@members.map { member => @members.map { member =>
addMemberHTML('@member.userName', @member.isManager); addMemberHTML('@member.userName', @member.manager);
} }
function addMemberHTML(userName, isManager){ function addMemberHTML(userName, isManager){

View File

@@ -14,9 +14,9 @@
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td @if(account.isRemoved){style="background-color: #dddddd;"}> <td @if(account.removed){style="background-color: #dddddd;"}>
<div class="pull-right"> <div class="pull-right">
@if(account.isGroupAccount){ @if(account.groupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a> <a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else { } else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a> <a href="@path/admin/users/@account.userName/_edituser">Edit</a>
@@ -25,16 +25,16 @@
<div class="strong"> <div class="strong">
@avatar(account.userName, 20) @avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isGroupAccount){ @if(account.groupAccount){
(Group) (Group)
} else { } else {
@if(account.isAdmin){ @if(account.administrator){
(Administrator) (Administrator)
} else { } else {
(Normal) (Normal)
} }
} }
@if(account.isGroupAccount){ @if(account.groupAccount){
@members(account.userName).map { userName => @members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true) @avatar(userName, 20, tooltip = true)
} }
@@ -42,7 +42,7 @@
</div> </div>
<div> <div>
<hr> <hr>
@if(!account.isGroupAccount){ @if(!account.groupAccount){
<i class="octicon octicon-mail"></i> @account.mailAddress <i class="octicon octicon-mail"></i> @account.mailAddress
} }
@account.url.map { url => @account.url.map { url =>
@@ -52,7 +52,7 @@
<div> <div>
<span class="muted">Registered:</span> @datetime(account.registeredDate) <span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate) <span class="muted">Updated:</span> @datetime(account.updatedDate)
@if(!account.isGroupAccount){ @if(!account.groupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime) <span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
} }
</div> </div>

View File

@@ -8,9 +8,11 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Issues"){ @html.main("Issues"){
@dashboard.html.tab("issues") <div class="body">
<div class="container"> @dashboard.html.tab("issues")
@issuesnavi(filter, "issues", condition) <div class="container">
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issuesnavi(filter, "issues", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div> </div>
} }

View File

@@ -8,9 +8,11 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main("Pull Requests"){ @html.main("Pull Requests"){
@dashboard.html.tab("pulls") <div class="body">
<div class="container"> @dashboard.html.tab("pulls")
@issuesnavi(filter, "pulls", condition) <div class="container">
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @issuesnavi(filter, "pulls", condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div>
</div> </div>
} }

View File

@@ -27,12 +27,6 @@ $(function(){
throw e; throw e;
} }
} }
// Adjust clickable area width
$('#@textareaId').next('div.clickable').css({
'width': ($('#@textareaId').width() + 18) + 'px',
'font-size': '13px'
});
}); });
</script> </script>
} }

View File

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

View File

@@ -1,8 +1,16 @@
@(id: String, value: String, prepend: Boolean = false)(html: Html) @(id: String, value: String, style: String = "")(html: Html = Html(""))
<div class="input-group @if(prepend){input-prepend}" style="margin-bottom: 0px;"> @if(html.body.nonEmpty){
@html <div class="input-group" style="margin-bottom: 0px;">
<span class="input-group-btn"><span id="@id" class="btn btn-sm btn-default" data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span></span> @html
</div> <span class="input-group-btn">
<span id="@id" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
</span>
</div>
} else {
<span id="@id" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
}
<script> <script>
// copy to clipboard // copy to clipboard
(function() { (function() {

View File

@@ -2,7 +2,7 @@
@import gitbucket.core.service.RepositoryService @import gitbucket.core.service.RepositoryService
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@if(repository.repository.isPrivate){ @if(repository.repository.`private`){
<i class="@{if(large){"mega-"}}octicon octicon-lock"></i> <i class="@{if(large){"mega-"}}octicon octicon-lock"></i>
} else { } else {
@if(repository.repository.originUserName.isDefined){ @if(repository.repository.originUserName.isDefined){

View File

@@ -4,79 +4,81 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@main("GitBucket"){ @main("GitBucket"){
@dashboard.html.tab() <div class="body">
<div class="container"> @dashboard.html.tab()
<div class="row"> <div class="container">
<div class="col-md-8"> <div class="row">
<div class="pull-right"> <div class="col-md-8">
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a> <div class="pull-right">
</div> <a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
@helper.html.activities(activities)
</div>
<div class="col-md-4">
@settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
@Html(information)
</div> </div>
} @helper.html.activities(activities)
@if(loginAccount.isEmpty){ </div>
@signinform(settings)
} else { <div class="col-md-4">
<div class="panel panel-default"> @settings.information.map { information =>
<div class="panel-heading strong"> <div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<div class="pull-right"> <button type="button" class="close" data-dismiss="alert">&times;</button>
<a href="@path/new" class="btn btn-success btn-sm">New repository</a> @Html(information)
</div>
Your repositories <span class="badge">@userRepositories.size</span>
</div> </div>
<ul class="list-group list-group-flush"> }
@if(userRepositories.isEmpty){ @if(loginAccount.isEmpty){
<li class="list-group-item">No repositories</li> <div id="dashboard-signin-form">@signinform(settings)</div>
} else { } else {
@defining(20){ max => <div class="panel panel-default">
@userRepositories.zipWithIndex.map { case (repository, i) => <div class="panel-heading strong">
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}"> <div class="pull-right">
@helper.html.repositoryicon(repository, false) <a href="@path/new" class="btn btn-success btn-sm">New repository</a>
@if(repository.owner == loginAccount.get.userName){ </div>
<a href="@url(repository)"><span class="strong">@repository.name</span></a> Your repositories <span class="badge">@userRepositories.size</span>
} else { </div>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> <ul class="list-group list-group-flush">
} @if(userRepositories.isEmpty){
</li> <li class="list-group-item">No repositories</li>
} } else {
@if(userRepositories.size > max){ @defining(20){ max =>
<li class="list-group-item show-more"> @userRepositories.zipWithIndex.map { case (repository, i) =>
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a> <li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
</li> @helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
}
</li>
}
@if(userRepositories.size > max){
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
</li>
}
} }
} }
} </ul>
</ul> </div>
</div> }
} <div class="panel panel-default">
<div class="panel panel-default"> <div class="panel-heading strong">Recent updated repositories</div>
<div class="panel-heading strong">Recent updated repositories</div> <ul class="list-group list-group-flush">
<ul class="list-group list-group-flush"> @if(recentRepositories.isEmpty){
@if(recentRepositories.isEmpty){ <li class="list-group-item">No repositories</li>
<li class="list-group-item">No repositories</li> } else {
} else { @defining(20){ max =>
@defining(20){ max => @recentRepositories.zipWithIndex.map { case (repository, i) =>
@recentRepositories.zipWithIndex.map { case (repository, i) => <li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}"> @helper.html.repositoryicon(repository, false)
@helper.html.repositoryicon(repository, false) <a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a> </li>
</li> }
} @if(recentRepositories.size > max){
@if(recentRepositories.size > max){ <li class="list-group-item show-more">
<li class="list-group-item show-more"> <a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a> </li>
</li> }
} }
} }
} </ul>
</ul> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<hr/><br/> <hr/><br/>
<form method="POST" validate="true"> <form method="POST" validate="true">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
@helper.html.preview( @helper.html.preview(

View File

@@ -7,7 +7,7 @@
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.model.CommitComment @import gitbucket.core.model.CommitComment
@if(issue.isDefined){ @if(issue.isDefined){
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div> <div class="issue-avatar-image">@avatarLink(issue.get.openedUserName, 48)</div>
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-heading"> <div class="panel-heading">
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span> @user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
@@ -36,7 +36,7 @@
case comment: gitbucket.core.model.IssueComment => { case comment: gitbucket.core.model.IssueComment => {
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch" @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
&& comment.action != "commit" && comment.action != "refer"){ && comment.action != "commit" && comment.action != "refer"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId"> <div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading"> <div class="panel-heading">
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
@@ -138,7 +138,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span> <span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
@avatar(comment.commentedUserName, 16) @avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
close @issueOrPullRequest() closed this @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</div> </div>
</div> </div>
@@ -210,7 +210,7 @@ $(function(){
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){ $(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
var url = '@url(repository)/commit_comments/_data/' + id; var url = '@url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.box')); var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
$.get(url, $.get(url,
{ {

View File

@@ -10,11 +10,11 @@
<form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group"> <form action="@url(repository)/issues/new" method="POST" validate="true" class="form-group">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-box"> <div class="panel panel-default issue-box">
<div class="panel-body"> <div class="panel-body">
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
<input type="text" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/> <input type="text" id="issue-title" name="title" class="form-control input-lg" value="" placeholder="Title" autofocus/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = "", content = "",
@@ -23,7 +23,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = hasWritePermission,
style = "width: 680px; height: 200px; max-height: 250px;", style = "height: 200px; max-height: 250px;",
elastic = true elastic = true
) )
<div class="align-right"> <div class="align-right">

View File

@@ -49,7 +49,8 @@
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="milestone-alert">Due by @date(dueDate)</span> <i class="octicon octicon-alert" style="color:#BD2C00;"></i>
<span class="milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due by @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }

View File

@@ -11,7 +11,7 @@
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span> <span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div> </div>
<script> <script>
$('div#label-color-@labelId').colorpicker(); $('div#label-color-@labelId').colorpicker({format: "hex"});
</script> </script>
<span class="pull-right"> <span class="pull-right">
<span id="label-error-@labelId" class="error"></span> <span id="label-error-@labelId" class="error"></span>

View File

@@ -41,7 +41,8 @@
} else { } else {
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<i class="octicon octicon-alert" style="color:#BD2C00;"></i><span class="muted milestone-alert">Due by @date(dueDate)</span> <i class="octicon octicon-alert" style="color:#BD2C00;"></i>
<span class="muted milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due by @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }

View File

@@ -36,7 +36,7 @@
<script src="@assets/vendors/facebox/facebox.js"></script> <script src="@assets/vendors/facebox/facebox.js"></script>
<script src="@assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script> <script src="@assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
@repository.map { repository => @repository.map { repository =>
@if(!repository.repository.isPrivate){ @if(!repository.repository.`private`){
<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" />
} }
} }
@@ -92,7 +92,7 @@
<ul class="dropdown-menu pull-right"> <ul class="dropdown-menu pull-right">
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li> <li><a href="@url(loginAccount.get.userName)">Your profile</a></li>
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li> <li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
@if(loginAccount.get.isAdmin){ @if(loginAccount.get.administrator){
<li><a href="@path/admin/users">System administration</a></li> <li><a href="@path/admin/users">System administration</a></li>
} }
<li><a href="@path/signout">Sign out</a></li> <li><a href="@path/signout">Sign out</a></li>

View File

@@ -1,30 +1,28 @@
@(active: String, @(active: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
id: Option[String] = None, id: Option[String] = None,
expand: Boolean = false, isRepoTop: Boolean = false,
isNoGroup: Boolean = true, isNoGroup: Boolean = true,
info: Option[Any] = None, info: Option[Any] = None,
error: Option[Any] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) error: Option[Any] = None)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.service.SystemSettingsService
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@sidemenu(path: String, name: String, icon: String, label: String, count: Int = 0) = { @menuitem(path: String, name: String, icon: String, label: String, count: Int = 0) = {
<li @if(active == name){class="active"} @if(!expand){data-toggle="tooltip" data-placement="left" data-original-title="@label"}> <li @if(active == name){class="active"}>
<a href="@url(repository)@path"> <a href="@url(repository)@path">
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i> <i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i> <span class="pc">@label</span>
@if(expand){ @label} @if(count > 0){
@if(expand && count > 0){ <span class="badge">@count</span>
<div class="pull-right"><span class="badge">@count</span></div>
} }
</a> </a>
</li> </li>
} }
<div class="container"> <div class="headbar">
@helper.html.information(info) <div class="container">
@helper.html.error(error) @helper.html.information(info)
<div class="row"> @helper.html.error(error)
<div class="head"> <div class="head">
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="input-group pull-right"> <div class="input-group pull-right">
@@ -60,87 +58,52 @@
} }
} }
</div> </div>
<ul class="headmenu">
@menuitem("" , "code" , "code" , "Code")
@menuitem("/issues", "issues" , "issue-opened" , "Issues", repository.issueCount)
@menuitem("/pulls" , "pulls" , "git-pull-request" , "Pull Requests", repository.pullCount)
@menuitem("/wiki" , "wiki" , "book" , "Wiki")
@if(loginAccount.isDefined && (loginAccount.get.administrator || repository.managers.contains(loginAccount.get.userName))){
@menuitem("/settings" , "settings" , "tools", "Settings")
}
</ul>
</div> </div>
</div> </div>
<hr style="margin-bottom: 20px;"/>
<div class="container body"> <div class="container body">
<div style="width: @if(expand){170px} else {40px};" class="pull-right"> @if(isRepoTop){
<ul class="sidemenu"> @repository.repository.description.map { description =>
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li> <p class="description">@detectAndRenderLinks(description)</p>
@sidemenu("" , "code" , "code" , "Code")
@sidemenu("/issues", "issues" , "issue-opened" , "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "git-pull-request" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "book" , "Wiki")
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@sidemenu("/settings" , "settings" , "tools", "Settings")
}
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li>
</ul>
@if(expand){
<div class="small">
<strong id="repository-url-proto">HTTP</strong> <span class="mute">clone URL</span>
</div>
@helper.html.copy("repository-url-copy", repository.httpUrl){
<input type="text" value="@repository.httpUrl" id="repository-url" class="form-control input-sm" readonly>
}
@if(settings.ssh && loginAccount.isDefined){
<div class="small">
<span class="mute">You can clone <a href="javascript:void(0);" id="repository-url-http">HTTP</a> or <a href="javascript:void(0);" id="repository-url-ssh">SSH</a>.</span>
</div>
}
@id.map { id =>
@if(context.platform != "linux" && context.platform != null){
<div style="margin-top: 10px;">
<a href="@repository.httpOpenRepoUrl(context.platform)" id="repository-clone-url" class="btn btn-sm btn-default btn-block"><i class="octicon octicon-desktop-download"></i>&nbsp;&nbsp;Clone in Desktop</a>
</div>
}
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-sm btn-default btn-block"><i class="octicon octicon-cloud-download"></i>Download ZIP</a>
</div>
@*
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-sm btn-default btn-block "><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
</div>
*@
}
} }
</div> <div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default">
<div class="pull-left" style="width: @if(expand){770px} else {895px};"> <table class="fill-width">
@if(expand){ <tr>
@repository.repository.description.map { description => <td style="width: 33%; text-align: center;">
<p class="description">@detectAndRenderLinks(description)</p> <a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link">
} <i class="octicon octicon-history"></i>
<div style="margin-bottom: 10px; padding: 4px;" class="panel panel-default"> @if(repository.commitCount > 10000){
<table class="fill-width"> <strong>10000+</strong> commits
<tr> } else {
<td style="width: 33%; text-align: center;"> <strong>@repository.commitCount</strong> commits
<a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link"> }
<i class="octicon octicon-history"></i> </a>
@if(repository.commitCount > 10000){ </td>
<strong>10000+</strong> commits <td style="width: 33%; text-align: center;">
} else { <a href="@url(repository)/branches" class="header-link" class="header-link">
<strong>@repository.commitCount</strong> commits <i class="octicon octicon-git-branch"></i>
} <strong>@repository.branchList.length</strong> branches
</a> </a>
</td> </td>
<td style="width: 33%; text-align: center;"> <td style="width: 33%; text-align: center;">
<a href="@url(repository)/branches" class="header-link" class="header-link"> <a href="@url(repository)/tags" class="header-link" class="header-link">
<i class="octicon octicon-git-branch"></i> <i class="octicon octicon-tag"></i>
<strong>@repository.branchList.length</strong> branches <strong>@repository.tags.length</strong> releases
</a> </a>
</td> </td>
<td style="width: 33%; text-align: center;"> </tr>
<a href="@url(repository)/tags" class="header-link" class="header-link"> </table>
<i class="octicon octicon-tag"></i> </div>
<strong>@repository.tags.length</strong> releases }
</a> @body
</td>
</tr>
</table>
</div>
}
@body
</div>
</div> </div>
<script> <script>
$(function(){ $(function(){
@@ -150,7 +113,7 @@ $(function(){
if(e.target.tagName == "I"){ if(e.target.tagName == "I"){
target = e.target.parentElement; target = e.target.parentElement;
} }
$(target).prev ('div.gradient' ).css('border-left', '1px solid silver'); $(target).prev('div.gradient').css('border-left', '1px solid silver');
}); });
$('ul.sidemenu a').mouseout(function(e){ $('ul.sidemenu a').mouseout(function(e){
@@ -158,7 +121,7 @@ $(function(){
if(e.target.tagName == "I"){ if(e.target.tagName == "I"){
target = e.target.parentElement; target = e.target.parentElement;
} }
$(target).prev ('div.gradient' ).css('border-left', '1px solid #eee'); $(target).prev('div.gradient').css('border-left', '1px solid #eee');
}); });
$('a[rel*=facebox]').facebox({ $('a[rel*=facebox]').facebox({
@@ -180,21 +143,5 @@ $(function(){
$('#fork-form').submit(); $('#fork-form').submit();
}); });
} }
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url-proto').text('HTTP');
$('#repository-url').val('@repository.httpUrl');
$('#repository-clone-url').attr('href', '@repository.httpOpenRepoUrl(context.platform)')
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
$('#repository-url-ssh').click(function(){
$('#repository-url-proto').text('SSH');
$('#repository-url').val('@repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-clone-url').attr('href', '@repository.sshOpenRepoUrl(context.platform, settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
});
}
}); });
</script> </script>

View File

@@ -17,7 +17,7 @@
<li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li> <li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li>
</ul> </ul>
<div> <div>
<div class="commit-avatar-image">@avatar(commit, 40)</div> <div class="commit-avatar-image">@avatarLink(commit, 40)</div>
<div> <div>
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a> <a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){ @if(commit.description.isDefined){

View File

@@ -55,11 +55,11 @@
<form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true"> <form method="POST" action="@path/@originRepository.owner/@originRepository.name/pulls/new" validate="true">
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-box"> <div class="panel panel-default issue-box">
<div class="panel-body"> <div class="panel-body">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
<input type="text" name="title" class="form-control input-lg" style="width: 680px" placeholder="Title"/> <input type="text" name="title" class="form-control input-lg" placeholder="Title"/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = "", content = "",
@@ -68,7 +68,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = true, hasWritePermission = true,
style = "width: 680px; height: 200px;" style = "height: 200px;"
) )
<input type="hidden" name="targetUserName" value="@originRepository.owner"/> <input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/> <input type="hidden" name="targetBranch" value="@originId"/>

View File

@@ -83,8 +83,8 @@
</div> </div>
@if(status.hasMergePermission){ @if(status.hasMergePermission){
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa"> <div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa">
<input type="button" class="btn @if(!status.hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/> <input type="button" class="btn btn-lg @if(!status.hasProblem){btn-success} else {btn-default}" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/>
You can also merge branches on the <a href="#" class="show-command-line">command line</a>. &nbsp;&nbsp;You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
<div id="command-line" style="display: none;margin-top: 15px;"> <div id="command-line" style="display: none;margin-top: 15px;">
<hr /> <hr />
@if(status.hasConflict){ @if(status.hasConflict){
@@ -100,24 +100,23 @@
you can perform a manual merge on the command line. you can perform a manual merge on the command line.
</p> </p>
} }
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){ @helper.html.copy("repository-url-copy", forkedRepository.httpUrl){
<div class="btn-group" data-toggle="buttons-radio"> <div class="input-group-btn" data-toggle="buttons">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button> <label class="btn btn-sm btn-default active" id="repository-url-http"><input type="radio" checked>HTTP</label>
@if(settings.ssh && loginAccount.isDefined){ @if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button> <label class="btn btn-sm btn-default" id="repository-url-ssh"><input type="radio">SSH</label>
} }
</div> </div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly /> <input type="text" class="form-control input-sm" value="@forkedRepository.httpUrl" id="repository-url" readonly />
} }
<div> <div style="margin-top: 10px;">
<p> <p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes. <span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
</p> </p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" + @defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command => s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){ @helper.html.copy("merge-command-copy-1", command, "position: absolute; right: 31px;")()
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre> <pre style="font-size: 12px; border-radius: 3px;" id="merge-command">@Html(command)</pre>
}
} }
</div> </div>
<div> <div>
@@ -126,9 +125,8 @@
</p> </p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" + @defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command => s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){ @helper.html.copy("merge-command-copy-2", command, "position: absolute; right: 31px;")()
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre> <pre style="font-size: 12px; border-radius: 3px;">@command</pre>
}
} }
</div> </div>
</div> </div>
@@ -141,7 +139,7 @@
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch} Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div> </div>
<span id="error-message" class="error"></span> <span id="error-message" class="error"></span>
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea> <textarea name="message" style="height: 80px;" class="form-control">@issue.title</textarea>
<div> <div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/> <input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/> <input type="submit" class="btn btn-success" value="Confirm merge"/>
@@ -171,27 +169,25 @@ $(function(){
$('#confirm-merge-form').show(); $('#confirm-merge-form').show();
}); });
@if(settings.ssh && loginAccount.isDefined){ @forkedRepository.sshUrl.map { sshUrl =>
$('#repository-url-http').click(function(){ $('#repository-url-http').click(function(e){
// Update URL box // Update URL box
$('#repository-url').val('@forkedRepository.httpUrl'); $('#repository-url').val('@forkedRepository.httpUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance // Update command guidance
$('#merge-command').text($('#merge-command').text().replace( $('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)', '@sshUrl', '@forkedRepository.httpUrl'
'@forkedRepository.httpUrl'
)); ));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text()); $('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
}); });
$('#repository-url-ssh').click(function(){ $('#repository-url-ssh').click(function(e){
// Update URL box // Update URL box
$('#repository-url').val('@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'); $('#repository-url').val('@sshUrl');
$('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val());
// Update command guidance // Update command guidance
$('#merge-command').text($('#merge-command').text().replace( $('#merge-command').text($('#merge-command').text().replace(
'@forkedRepository.httpUrl', '@forkedRepository.httpUrl', '@sshUrl'
'@forkedRepository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), loginAccount.get.userName)'
)); ));
$('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text()); $('#merge-command-copy-1').attr('data-clipboard-text', $('#merge-command').text());
}); });

View File

@@ -21,7 +21,9 @@
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a> <a class="btn" href="#" id="edit">Edit</a>
} }
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a> @if(loginAccount.isDefined){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
</div> </div>
<div class="edit-title pull-right" style="display: none;"> <div class="edit-title pull-right" style="display: none;">
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a> <a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
@@ -65,7 +67,7 @@
} }
</div> </div>
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab"> <ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{ <li><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{
case comment: IssueComment => Some(comment) case comment: IssueComment => Some(comment)
case _: CommitComment => None case _: CommitComment => None
}.size</span></a></li> }.size</span></a></li>
@@ -73,7 +75,7 @@
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li> <li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
</ul> </ul>
<div class="tab-content fill-width pull-left"> <div class="tab-content fill-width pull-left">
<div class="tab-pane active" id="conversation"> <div class="tab-pane" id="conversation">
@flash.get("error").map{ error => @flash.get("error").map{ error =>
<div class="alert alert-error">@error</div> <div class="alert alert-error">@error</div>
} }
@@ -94,6 +96,22 @@
} }
<script> <script>
$(function(){ $(function(){
// Determine active tab from hash
if(location.hash == '#commits'){
$('li:has(a[href=#commits])').addClass('active');
$('div#commits').addClass('active');
} else if(location.hash == '#files'){
$('li:has(a[href=#files])').addClass('active');
$('div#files').addClass('active');
} else {
$('li:has(a[href=#conversation])').addClass('active');
$('div#conversation').addClass('active');
}
// Set hash when tab is clicked
$('ul.nav-tabs li a').click(function(e){
location.href = $(e.delegateTarget).attr("href");
});
$('#pullreq-tab a').click(function (e) { $('#pullreq-tab a').click(function (e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show'); $(this).tab('show');

View File

@@ -11,7 +11,7 @@
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group"> <div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="octicon octicon-list-unordered"></i></a> <a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t">Find file</a>
</div></div> </div></div>
<div class="line-age-legend"> <div class="line-age-legend">
<span>Newer</span> <span>Newer</span>

View File

@@ -11,7 +11,7 @@
@if(!fileName.isDefined){<hr/><br/>} @if(!fileName.isDefined){<hr/><br/>}
<form method="POST" validate="true" style="max-width: 874px;"> <form method="POST" validate="true" style="max-width: 874px;">
@if(!fileName.isDefined){ @if(!fileName.isDefined){
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
} }
<div class="panel panel-default issue-comment-box"> <div class="panel panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
@@ -23,7 +23,7 @@
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = hasWritePermission,
style = "width: 635px; height: 100px; max-height: 150px;", style = "height: 100px; max-height: 150px;",
elastic = true elastic = true
) )
</div> </div>

View File

@@ -46,7 +46,7 @@
<li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li> <li><a href="@url(repository)/tree/@commit.id" style="line-height: 16px;"><i class="octicon octicon-code link"></i></a></li>
</ul> </ul>
<div> <div>
<div class="commit-avatar-image">@avatar(commit, 40)</div> <div class="commit-avatar-image">@avatarLink(commit, 40)</div>
<div> <div>
<a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a> <a href="@url(repository)/commit/@commit.id" class="commit-message" style="font-weight: bold;">@link(commit.summary, repository)</a>
@if(commit.description.isDefined){ @if(commit.description.isDefined){

View File

@@ -33,7 +33,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatarLink(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-content"> <div class="box-content">
<div> <div>

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