Compare commits

...

212 Commits
3.1 ... 3.5

Author SHA1 Message Date
Naoki Takezoe
836fa47812 GitBucket 3.5.0 release 2015-08-01 01:30:32 +09:00
Naoki Takezoe
77b3650580 (refs #844)Improve global header menu 2015-07-28 21:53:51 +09:00
Naoki Takezoe
67ee6857ad Remove unnecessary spec 2015-07-28 11:16:02 +09:00
Naoki Takezoe
5ab15c0a14 Merge branch 'beraboris-implement-668' 2015-07-28 11:13:51 +09:00
Naoki Takezoe
96a3f2c301 (refs #810)Some fix about pull request 2015-07-28 11:12:11 +09:00
Naoki Takezoe
85707264c4 Fix position of fork-form 2015-07-28 02:04:49 +09:00
Naoki Takezoe
ed05422ea8 Merge branch 'implement-668' of https://github.com/beraboris/gitbucket into beraboris-implement-668 2015-07-28 01:58:30 +09:00
Naoki Takezoe
8f10c8051e (refs #810)Display Compare (or Pull Request) button for the default branch of the forked repository 2015-07-28 01:57:54 +09:00
Naoki Takezoe
41fff399b5 Redirect to the repository after sign-in by clicking fork button 2015-07-26 03:17:13 +09:00
Naoki Takezoe
9e237647b0 Small fix about icons 2015-07-26 02:59:26 +09:00
Naoki Takezoe
1be53c6746 Merge branch 'sapk-master' 2015-07-23 02:11:17 +09:00
Naoki Takezoe
f2368b03c0 (refs #409)Fix header anchor name in Markdown 2015-07-23 00:53:29 +09:00
Naoki Takezoe
95284c0b36 (refs #409)Fix Markdown style 2015-07-22 02:42:30 +09:00
Naoki Takezoe
f1e21a93fb (refs #409)Fix icons 2015-07-22 02:42:30 +09:00
Naoki Takezoe
689811f659 Add forks icon 2015-07-19 23:23:00 +09:00
Naoki Takezoe
33ccb4e98c Merge branch 'uli-heller-email-sender' 2015-07-19 02:15:03 +09:00
Naoki Takezoe
29f6e98f9c Merge branch 'email-sender' of https://github.com/uli-heller/gitbucket into uli-heller-email-sender 2015-07-19 02:14:36 +09:00
Naoki Takezoe
775c8cc064 Update release operation 2015-07-19 02:04:52 +09:00
Naoki Takezoe
9a974d047c Small fix 2015-07-19 02:01:15 +09:00
Naoki Takezoe
3fd252d2db Remove unnecessary file 2015-07-19 01:47:42 +09:00
Naoki Takezoe
43edb034c8 Merge branch 'linux-ubuntu1404' of https://github.com/uli-heller/gitbucket into uli-heller-linux-ubuntu1404 2015-07-19 01:46:40 +09:00
Naoki Takezoe
e629ca391e Improve env.sh to extract GITBUCKET_VERSION from build.scala 2015-07-19 01:42:42 +09:00
Naoki Takezoe
0db7eba3f2 Merge pull request #836 from shiena/patch/fix_contenttype
Fix duplicated Content-Type
2015-07-18 23:30:27 +09:00
Naoki Takezoe
a472abc88e Merge pull request #829 from muddydixon/fix/group_image_cursor_css
fixed cursor pointer css of group image change
2015-07-18 23:25:04 +09:00
Naoki Takezoe
861c619c19 Merge branch 'master' of https://github.com/sapk/gitbucket into sapk-master 2015-07-18 22:18:02 +09:00
Naoki Takezoe
124f331963 Fix testcase 2015-07-18 22:01:45 +09:00
Mitsuhiro Koga
76fcd44191 Fix duplicated Content-Type 2015-07-16 13:05:40 +09:00
Naoki Takezoe
2124c0b88c Ignore ensime files 2015-07-14 02:49:21 +09:00
Naoki Takezoe
2f5ab8e3b9 (refs #830)Bump pegdown to 1.5.0 and support strike syntax 2015-07-14 02:47:26 +09:00
muddydixon
32c8b6914b fixed cursor pointer css of group image change 2015-07-13 16:54:12 +09:00
Naoki Takezoe
7e9d940f64 Merge branch 'beraboris-better-atom-titles' 2015-07-05 22:17:27 +09:00
Naoki Takezoe
59e826b630 (refs #811)Remove html tags from title of atom feed 2015-07-05 22:15:39 +09:00
Naoki Takezoe
88f3ee4b13 Merge branch 'better-atom-titles' of https://github.com/beraboris/gitbucket into beraboris-better-atom-titles 2015-07-05 21:37:40 +09:00
Naoki Takezoe
b7380a084e Merge pull request #814 from uli-heller/patch-1
Fixes #748 - duplicate c3p0.war
2015-07-05 21:33:36 +09:00
Naoki Takezoe
cb65e790ae (refs #812)Apply GitRepositoryFilter to SSH access too 2015-07-05 15:38:55 +09:00
Naoki Takezoe
573eabee93 (refs #812)Prepend '/' to repository name 2015-07-05 15:08:52 +09:00
Naoki Takezoe
f0d4c6546a (refs #812)SSH support for plug-in served git repository 2015-07-05 14:16:16 +09:00
uli-heller
b0943c87c8 Fixes #748 - duplicate c3p0.war 2015-07-05 06:16:42 +02:00
Boris Bera
4f1208ea98 Atom feed now puts the activity message in title
Originally it used to put the activity type in the message. This gave
no useful information. The user had to open the news item to see what
was going on. Now, the title is the activity's message rendered to html.
Note that this is the same as the content of the news item.

This fixes #481
2015-07-04 15:29:09 -04:00
Naoki Takezoe
02f16639ea Remove twirl-compiler from dependency 2015-07-05 00:50:27 +09:00
Naoki Takezoe
cd5e28c0b8 (refs #812)Add GitRepositoryFilter for authentication 2015-07-05 00:13:34 +09:00
Naoki Takezoe
6f7579f8d9 (refs #812)Add new extension point to serve Git repository by plug-in 2015-07-04 17:42:13 +09:00
Boris Bera
f1d2a71b49 Fixed url generated by pull request/compare button
The `:` character was getting escaped leading to an ugly url. This is
different from the urls generated when editing the branches in a pull
request. In that case the url did not have `:` escaped.
2015-07-03 21:23:53 -04:00
Boris Bera
fd4fe0dc0d Pullreqs in forks now use upstream master as origin
This implements #668.
2015-07-03 20:56:06 -04:00
Naoki Takezoe
3d5e4a4225 (refs #802)Allow '+' in repository name 2015-07-03 21:40:26 +09:00
Naoki Takezoe
10ffb452d0 Fix delete branch confirmation message 2015-07-03 21:19:14 +09:00
Naoki Takezoe
3218bddbdc (refs #808)Add collaborators when fork repository by group 2015-07-03 20:47:37 +09:00
Naoki Takezoe
59f78dcbcb Fix behavior when enableAnchor is false 2015-06-28 03:43:15 +09:00
Naoki Takezoe
0fe062a02f Add option to disable anchor for headline in markdown 2015-06-28 03:20:55 +09:00
Naoki Takezoe
869eaf8cfd Update version to 3.5.0-SNAPSHOT 2015-06-27 23:09:12 +09:00
Naoki Takezoe
5b445c9736 Improve declarative Plugin definition interface 2015-06-27 20:54:19 +09:00
Naoki Takezoe
6f3f3eaa99 Fix Wiki font style of Wiki editing form 2015-06-27 19:41:48 +09:00
Naoki Takezoe
a23ce92676 Add pom.xml for deploying assemble jar 2015-06-27 19:09:43 +09:00
Uli Heller
ad55d5199d Use '{loginName} {fromAddress}' for the from field within the notification emails 2015-06-27 08:45:27 +02:00
Uli Heller
bd05895761 Make the release process work on linux 2015-06-27 07:11:23 +02:00
Uli Heller
c68dd6c891 Another change to make it work on linux - now linux and windows commands are mixed up unfortunately... 2015-06-27 06:56:12 +02:00
uli-heller
daae9ae1e7 Update how_to_run.md
Once "bashism-source" is applied, the build runs smoothly on linux, no need to change anything...
2015-06-27 06:56:12 +02:00
Uli Heller
33dde75ae1 doc change: build.xml -> release/build.xml 2015-06-27 06:56:12 +02:00
Uli Heller
b2e1e1796d Fixed bashism 'source' 2015-06-27 06:16:22 +02:00
Naoki Takezoe
c4a682773c Fix warnings 2015-06-27 10:18:22 +09:00
Naoki Takezoe
07e1873cb3 Update release.md 2015-06-27 10:14:30 +09:00
Naoki Takezoe
14d0c4dc32 GitBucket 3.4 release 2015-06-27 10:12:25 +09:00
Naoki Takezoe
19f79b6e54 Merge pull request #789 from milligramme/patch-1
modify css: hide href content on printing
2015-06-26 17:47:31 +09:00
Naoki Takezoe
e0b9fc9481 (refs #788)Fix NotFound in comparing with sibling repository 2015-06-26 17:06:33 +09:00
milligramme
16c28ad938 modify css: hide href content on printing
on printing, href content appeared. 
it is caused by bootstrap.css

faae237ac5/src/main/webapp/assets/vendors/bootstrap/css/bootstrap.css (L183-L185)
2015-06-26 15:31:15 +09:00
Naoki Takezoe
0e5af9ffa9 Update README.md 2015-06-23 14:29:32 +09:00
Naoki Takezoe
a0f6f03d4e Add IntelliJ IDEA logo 2015-06-23 14:18:05 +09:00
Naoki Takezoe
f4cdfc5f32 (refs #769)go-import meta tag is generated for only public repos 2015-06-10 23:18:35 +09:00
Naoki Takezoe
b45988705b (refs #769)Add go-import meta tag 2015-06-10 23:14:12 +09:00
Naoki Takezoe
dd50239759 (refs #780)Not add an issue refer comment if it already exists 2015-06-09 10:32:58 +09:00
Naoki Takezoe
7297a07466 (refs #751)Fix issue reference in verbatim node 2015-06-09 02:24:45 +09:00
Naoki Takezoe
e5fa43f91c Merge pull request #778 from team-lab/fix/diff-ignore-space
fix diff ignore space
2015-06-08 13:22:25 +09:00
nazoking
88080c8d02 fix diff ignore space 2015-06-08 01:33:22 +09:00
Naoki Takezoe
d38dfee540 Remove empty lines 2015-06-07 22:38:03 +09:00
Naoki Takezoe
4da6a7b6f9 Merge pull request #768 from Mura-Mi/avoid-npe-masterless
Avoid NPE when master branch does not exist
2015-06-06 03:34:34 +09:00
Naoki Takezoe
957dfeed6d (refs #775)Move isRenderable() to helpers from PluginRepository 2015-06-05 01:34:23 +09:00
Naoki Takezoe
aa5a07b98e (refs #775)Add new extension point to add markup render 2015-06-05 01:30:00 +09:00
Naoki Takezoe
fd1ee07297 Provides declarative style plug-in definition. 2015-06-03 02:18:07 +09:00
Naoki Takezoe
07026125a9 Merge remote-tracking branch 'origin/master' 2015-06-02 01:06:21 +09:00
Naoki Takezoe
badb747d24 (refs #773)Fix url processing in the blame view 2015-06-02 01:06:02 +09:00
Naoki Takezoe
e27c20d600 Merge pull request #771 from bfritz/fix-release-typo
Fix typo in release.md
2015-06-01 08:27:26 +09:00
Naoki Takezoe
eb46c94c7c Merge pull request #772 from xuwei-k/fix-travis-fail
fix .travis.yml
2015-06-01 07:26:16 +09:00
xuwei-k
2e58f82a61 fix .travis.yml
```
java.lang.RuntimeException: Setting value cannot be null: {file:/home/travis/build/takezoe/gitbucket/}gitbucket/*:version
	at scala.sys.package$.error(package.scala:27)
	at sbt.EvaluateSettings$INode.setValue(INode.scala:143)
	at sbt.EvaluateSettings$MixedNode.evaluate0(INode.scala:175)
```
2015-06-01 04:04:45 +09:00
Brad Fritz
df28013b18 Fix typo in release.md 2015-05-31 11:53:08 -04:00
Naoki Takezoe
8f4e285974 Update README.md 2015-05-31 18:37:12 +09:00
Naoki Takezoe
08c4eeeefe (refs #770)Update release.md 2015-05-31 17:33:36 +09:00
Naoki Takezoe
1fdc2c629c (refs #770)Add set assembly invocation to deploy-assembly-jar.sh 2015-05-31 17:33:21 +09:00
Naoki Takezoe
a6767a1c3d (refs #770)Gather release scripts 2015-05-31 16:58:51 +09:00
Naoki Takezoe
c539d6b643 Update build.xml 2015-05-31 12:21:07 +09:00
Naoki Takezoe
80c3ffdfd7 Update release.md 2015-05-31 12:18:19 +09:00
Naoki Takezoe
f80f2106fe Fix deploy script 2015-05-31 11:58:02 +09:00
Naoki Takezoe
cbfae66882 Fix version 2015-05-31 11:52:15 +09:00
Naoki Takezoe
8a5014fcbe Update release.md 2015-05-31 11:49:35 +09:00
Naoki Takezoe
54d1bff213 Bump version to 3.3 2015-05-31 11:44:01 +09:00
Naoki Takezoe
0075664b9a Merge pull request #764 from team-lab/feature/file-finder-button-on-blob-view
add file finder button on blob view.
2015-05-31 11:26:28 +09:00
Mura-Mi
a2c26f0f2c Avoid NPE when master branch does not exist 2015-05-29 01:28:11 +09:00
nazoking
45f41a13e4 add file finder button on blob view. 2015-05-27 13:59:36 +09:00
Naoki Takezoe
faae237ac5 Update README.md 2015-05-26 01:56:57 +09:00
Naoki Takezoe
e01758e74c Update README.md 2015-05-25 23:29:20 +09:00
Naoki Takezoe
db7dd31c79 Merge branch 'sapk-fix-mobile' 2015-05-25 22:57:39 +09:00
Naoki Takezoe
7d7ac5e2be Merge branch 'fix-mobile' of https://github.com/sapk/gitbucket into sapk-fix-mobile
# Conflicts:
#	src/main/webapp/assets/common/css/gitbucket.css
2015-05-25 22:57:24 +09:00
Naoki Takezoe
577016a33f (refs #762)Add meta-tag to disable IE backward compatibility mode 2015-05-25 22:12:12 +09:00
Naoki Takezoe
fdf2102923 (refs #763)Don't remove disabled user's data and repositories. 2015-05-25 22:03:40 +09:00
Naoki Takezoe
8b47e57be0 Merge branch 'team-lab-featuer/image-on-diff' 2015-05-25 02:06:36 +09:00
Naoki Takezoe
8dad6b64b0 Merge branch 'featuer/image-on-diff' of https://github.com/team-lab/gitbucket into team-lab-featuer/image-on-diff
# Conflicts:
#	src/main/webapp/assets/common/js/gitbucket.js
2015-05-25 02:06:14 +09:00
Naoki Takezoe
05d36abdab Merge branch 'team-lab-feature/blame' 2015-05-25 02:00:49 +09:00
Naoki Takezoe
04e31c5b4f Merge branch 'feature/blame' of https://github.com/team-lab/gitbucket into team-lab-feature/blame
# Conflicts:
#	src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
#	src/main/webapp/assets/common/css/gitbucket.css
2015-05-25 02:00:27 +09:00
Naoki Takezoe
6470428a85 Merge pull request #696 from team-lab/feature/file-finder
add file finder
2015-05-25 01:49:45 +09:00
Naoki Takezoe
a7efb3989a Merge pull request #760 from sugamasao/add-pre-tag-horizontal-scroll
Add pre-tag horizontal scroll on markdown
2015-05-21 20:28:24 +09:00
sugamasao
32072d0bbf Add pre-tag horizontal scroll on markdown 2015-05-20 18:22:53 +09:00
Antoine GIRARD
78a3e4454d Fix mobile view 2015-05-18 21:37:07 +02:00
Antoine GIRARD
1ff68111b4 Merge remote-tracking branch 'upstream/master' 2015-05-18 15:51:03 +02:00
Naoki Takezoe
e576178e1e Merge pull request #758 from sugamasao/fix-height-of-diff-line
`_` is hidden by diff line
2015-05-17 00:26:19 +09:00
Naoki Takezoe
0f8bc2b03d Merge pull request #755 from tomomura/fix/api-user-login-value
fix login value for Webhook and API.
2015-05-17 00:25:32 +09:00
sugamasao
43565458d4 Fix fix height of diff line
`_` is hidden by diff line
2015-05-15 15:37:36 +09:00
tomomura
71cc3be6d5 fix login value. 2015-05-13 11:46:02 +09:00
Naoki Takezoe
9a8eef7b19 (refs #738)Set "application/octet-stream" as Content-Type to avoid automatic charset detection for text files in Scalatra 2015-05-10 17:36:26 +09:00
Naoki Takezoe
a08c4368b7 Revert dependency 2015-05-10 16:32:21 +09:00
Naoki Takezoe
3b456b2aab Move deploy-assembly-jar script 2015-05-10 15:36:27 +09:00
nazoking
31559418ba add image diff 2015-05-09 01:50:51 +09:00
Naoki Takezoe
692a6e43bc Update release.md 2015-05-07 11:30:41 +09:00
Naoki Takezoe
af4cce654c Update release.md 2015-05-07 11:29:41 +09:00
Naoki Takezoe
c63b02fd4a Move ERD and SVG file to /doc from /etc 2015-05-07 11:22:27 +09:00
Naoki Takezoe
e6974b6e51 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-05-07 11:14:25 +09:00
Naoki Takezoe
da3b7dbeff Improve assembly-jar deploying 2015-05-07 11:14:17 +09:00
Naoki Takezoe
9737bd7012 (refs #739)Update document 2015-05-07 11:08:11 +09:00
Naoki Takezoe
55fd8e5e2d (refs #739)Update document 2015-05-07 11:06:32 +09:00
Naoki Takezoe
9c4f181d93 (refs #739)Document for release operation 2015-05-07 10:55:59 +09:00
Naoki Takezoe
374342cfc1 Merge pull request #718 from team-lab/feature/api-add-issue-urls
Feature/api add issue urls
2015-05-06 21:19:59 +09:00
Naoki Takezoe
6fbdd237d1 Merge pull request #731 from ndarilek/master
Implement additional accessibility fixes
2015-05-06 10:46:13 +09:00
Naoki Takezoe
4e78f01a09 Dispose ObjectLoader certainty 2015-05-06 10:30:53 +09:00
Naoki Takezoe
8853264808 Merge pull request #744 from jochembroekhoff/master
jQuery updated to v1.11.1
2015-05-06 09:09:07 +09:00
Jochem Broekhoff
6db9b8038f jQuery updated to v1.11.1 2015-05-05 09:59:23 +02:00
Naoki Takezoe
c73e89ccd4 Update version to 3.2.0 2015-05-03 01:45:43 +09:00
Naoki Takezoe
f58a506780 Define Version 3.2 2015-05-03 01:26:35 +09:00
Naoki Takezoe
411d19e74e Update README.md 2015-05-03 01:25:55 +09:00
Naoki Takezoe
bd51ffd9d2 (refs #660)Register Scalatra error callback to rollback transaction 2015-05-03 01:14:22 +09:00
Naoki Takezoe
83980fdccd (refs #534)Allow to download large file 2015-05-02 23:09:18 +09:00
Naoki Takezoe
baab243bc8 Merge pull request #716 from expf/fallback_message
Fix style of message for browsers that do not support drag'n'drop file u...
2015-04-30 19:36:09 +09:00
Naoki Takezoe
4dd8e1dc63 (refs #707)Exclude disabled users from completion list 2015-04-30 16:19:04 +09:00
shimamoto
0b11b8b084 Merge pull request #732 from takezoe/delete_old_activity
(refs #441) Add limit of activity log configuration
2015-04-29 23:16:10 +09:00
Naoki Takezoe
704775dc60 (refs #441)Fix delete condition 2015-04-29 20:19:20 +09:00
Naoki Takezoe
c7a7be1de0 (refs #441)Fix testcase 2015-04-29 15:26:14 +09:00
Naoki Takezoe
e21a970977 (refs #441)Add limit of activity log configuration 2015-04-29 12:57:57 +09:00
Naoki Takezoe
a526dcf2dd Separate AutoUpdate from InitializeListener 2015-04-29 09:37:29 +09:00
Nolan Darilek
a7b48d63e4 Add autofocus to forms at strategic places. 2015-04-28 13:16:43 -05:00
Nolan Darilek
3677906e95 Label additional icons. 2015-04-28 13:11:50 -05:00
Naoki Takezoe
e86710fbbd Merge pull request #728 from kaakaa/testhook-npe
fix test hook error
2015-04-28 02:21:18 +09:00
kaakaa
73e850493a fix test hook error 2015-04-25 16:10:52 -04:00
Naoki Takezoe
7d735f6f8a Merge pull request #725 from sebastiancadena/patch-1
Fix typo
2015-04-24 19:41:11 +09:00
Juan Sebastian Cadena
043e99a9eb Fix typo
Repositories
2015-04-23 19:24:47 -05:00
Naoki Takezoe
65e079b1d3 Merge pull request #722 from ndarilek/master
Label unlabelled icons for accessibility.
2015-04-24 01:58:15 +09:00
Naoki Takezoe
32e6f584d8 Merge pull request #723 from team-lab/fix/#721-npe-getFileList-multi-origin-subfolder
(refs #721)NullPointerException on getFileList
2015-04-24 01:57:01 +09:00
nazoking
ebc5219ce6 (refs #721)NullPointerException on getFileList if branch has multi-origin and get sub-folder file list 2015-04-22 13:45:31 +09:00
Nolan Darilek
ad8e620bdf Label unlabelled icons for accessibility. 2015-04-21 17:38:19 -05:00
Naoki Takezoe
8590c693b9 (refs #715)Escape $ in replacement string 2015-04-22 01:20:11 +09:00
nazoking
5568acc5f3 add html_url on api issue-comment 2015-04-20 14:32:16 +09:00
nazoking
c467594199 add html_url and comments_url on api issue 2015-04-20 13:19:17 +09:00
Kohei Ichioka
8b29bf7d93 Fix style of message for browsers that do not support drag'n'drop file uploads 2015-04-20 10:24:41 +09:00
Naoki Takezoe
43b7f83082 (refs #713)Fix anchor to headline in Wiki 2015-04-20 00:24:49 +09:00
Naoki Takezoe
9ef5c981ef (refs #666)Remove html5shiv.js reference 2015-04-19 16:49:11 +09:00
Naoki Takezoe
f027ac34d4 Merge pull request #697 from banjun/fix-basicauth-for-public-repo
let non-anonymous user read access to public repos with basic auth
2015-04-16 02:13:33 +09:00
Naoki Takezoe
2ea31d6869 Revert "Fix basic auth read when anon access disabled"
This reverts commit 5f3d6242fd.
2015-04-16 02:12:40 +09:00
Naoki Takezoe
a1af7d0f9c Merge pull request #702 from marklacroix/no-anon-basic-auth-read
Fix basic auth read when anon access disabled
2015-04-16 02:05:50 +09:00
Naoki Takezoe
797bf37bfa Merge pull request #705 from team-lab/fix/word-diff-probrem
fix word-diff javascript error
2015-04-16 01:52:54 +09:00
Shintaro Murakami
7f78815c11 (refs #712) Fix problems of showing inline notes. 2015-04-13 19:31:15 +09:00
Naoki Takezoe
d8a3f308ed Merge pull request #709 from jparound30/fix_db_conn_handling
Fix db connection error in dev mode
2015-04-11 22:07:42 +09:00
jparound30
74e18a982d Fix db connection error in dev mode
Changes to close the no longer needed DB connections.
2015-04-11 13:32:29 +09:00
nazoking
e86ad423c5 fix word-diff javascript error (error occured if dictionary contains the word 'hasOwnProperty') 2015-04-09 22:18:46 +09:00
Naoki Takezoe
2f00060c57 Merge pull request #699 from rlazoti/fix-notifier
Fix issue notifications
2015-04-09 20:07:11 +09:00
Naoki Takezoe
3b7e6aa68e Merge pull request #701 from team-lab/fix/api-issue-user
fix some api user is invalid, and add content-type on webhook
2015-04-09 02:27:16 +09:00
Mark LaCroix
5f3d6242fd Fix basic auth read when anon access disabled
Basic authentication fails when not updating a public repository
with the "Disable Anonymous Access" option enabled
2015-04-08 11:46:44 -04:00
nazoking
6793a86bae fix some api user is invalid
issue.userName -> issue.openedUserName
  issueComment.userName -> issueComment.commentedUserName
2015-04-08 20:16:47 +09:00
Rodrigo Lazoti
d99ce20529 fix issue notifications 2015-04-07 10:56:52 -03:00
banjun
93e7b604cd let non-anonymous user read access to public repos with basic auth 2015-04-06 22:15:17 +09:00
nazoking
59c18056fc add file finder 2015-04-06 21:22:03 +09:00
Naoki Takezoe
5c81ce9b68 Add SystemSettings to Plugin arguments 2015-04-05 22:46:48 +09:00
Naoki Takezoe
c9cf62701f Merge pull request #690 from noc06140728/fix-basic-auth
Allow a password that contains colons
2015-04-05 17:04:43 +09:00
Naoki Takezoe
f65babca4b Merge branch 'team-lab-feature/#422-compare-link-near-branch-selector' 2015-04-05 16:53:09 +09:00
Naoki Takezoe
23c146fc5d Merge branch 'feature/#422-compare-link-near-branch-selector' of https://github.com/team-lab/gitbucket into team-lab-feature/#422-compare-link-near-branch-selector
Conflicts:
	src/main/twirl/gitbucket/core/repo/files.scala.html
2015-04-05 16:52:47 +09:00
Naoki Takezoe
e14f336142 Merge pull request #694 from team-lab/feature/#536-Commits-view-on-directory
Browse history for directory
2015-04-05 16:46:20 +09:00
nazoking
54647be5bd (#536) Browse commits for directory 2015-04-04 11:13:07 +09:00
Naoki Takezoe
9517d65646 Update README.md 2015-04-04 02:55:15 +09:00
Naoki Takezoe
0156e401fa Update README.md for 3.1.1 release 2015-04-04 02:39:25 +09:00
Naoki Takezoe
d6e2bc464d Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-04-03 22:39:29 +09:00
Naoki Takezoe
5124ff593d Update version number to 3.1.1 2015-04-03 22:39:17 +09:00
Naoki Takezoe
727c90afdc Merge pull request #681 from team-lab/feature/improve-file-list-performance
Feature/improve file list performance
2015-04-03 22:32:51 +09:00
Naoki Takezoe
9ebd5e3265 Merge pull request #691 from team-lab/fix/#684-api-contextpath-support
fix/api context-path support
2015-04-03 22:31:09 +09:00
nazoking
d120142127 (ref #684)fix api context-path support 2015-04-02 16:53:30 +09:00
Naoki Takezoe
f9e4cddcaf Update version number to 3.1.1 2015-04-02 11:10:29 +09:00
Naoki Takezoe
8f64f174d9 Rolled back H2 to 1.4.180 from 1.4.186 2015-04-02 11:08:44 +09:00
Masahiro Namba
d6817796b3 Allow a password that contains colons
Allow a password that contains colons in the basic autentication.
2015-04-01 02:11:05 +09:00
nazoking
ab19d473c4 (refs #442)show compare/pull request link near branch link. 2015-03-30 21:16:42 +09:00
Naoki Takezoe
48f0116358 Fix compilation error 2015-03-29 03:37:48 +09:00
Naoki Takezoe
d8e6e97845 Pass ServletContext to Plugin 2015-03-29 03:26:06 +09:00
Naoki Takezoe
9a8920788c Update gutbucket version to 3.1.0 2015-03-29 02:27:28 +09:00
nazoking
27864a3a3c Performance Improve for getFileList 2015-03-29 01:21:10 +09:00
nazoking
b39e863591 add test for getFileList 2015-03-29 01:12:33 +09:00
nazoking
d8d18ed25c Merge branch 'master' into feature/blame
Conflicts:
	src/main/scala/gitbucket/core/util/JGitUtil.scala
2015-03-28 21:19:40 +09:00
Naoki Takezoe
7661e8cadd Update assembly jar version to 3.1.0 2015-03-28 17:24:09 +09:00
nazoking
83fd2648f5 follow rename 2015-03-27 21:19:02 +09:00
nazoking
8e81758941 fix link 2015-03-27 21:06:12 +09:00
nazoking
41a6a29771 fix for ie 7,8,9 2015-03-27 21:03:41 +09:00
nazoking
3e0a50926f resolve gravatar on blame 2015-03-09 23:42:04 +09:00
Antoine GIRARD
dff816324d Rebase + Remove all unused image 2015-03-08 17:15:48 +01:00
Antoine GIRARD
a33e2c6e36 Completely moved to octicon 2015-03-04 23:46:46 +01:00
Antoine GIRARD
75ef82d18a ... 2015-03-04 23:46:46 +01:00
Antoine GIRARD
eb3c522122 Moved to octicon 2015-03-04 23:41:26 +01:00
Antoine GIRARD
6c8bcfc62e Import of octicons 2015-03-04 23:10:55 +01:00
nazoking
e408eb43bb fix parent. only path exists 2015-03-04 03:37:46 +09:00
nazoking
dc0aa0851e implove source-line-num performance.
and Stop scroll when click line number.
2015-03-04 03:14:22 +09:00
nazoking
51d7c43489 add blame 2015-03-04 01:52:07 +09:00
171 changed files with 10736 additions and 6659 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
*.class
*.log
.ensime
.ensime_cache
# sbt specific
dist/*

View File

@@ -1,3 +1,5 @@
language: scala
scala:
- 2.11.6
sudo: false
script:
- . env.sh
- sbt test

View File

@@ -54,20 +54,22 @@ For Installation on Windows Server with IIS see [this wiki page](https://github.
### Mac OS X
#### Installing Via Homebrew
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
```
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
```
#### Manual Installation
On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
@@ -79,6 +81,33 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 3.5 - 1 Aug 2015
- Octicons has been applied
- Global header has been enhanced. Now it's further similar to GitHub.
- Default compare / pull request target has been changed to the parent repository
- A lot of updates for [gitbucket-gist-plugin](https://github.com/takezoe/gitbucket-gist-plugin)
### 3.4 - 27 Jun 2015
- Declarative style plug-in definition
- New extension point to add markup render
- go-import support
### 3.3 - 31 May 2015
- Rich graphical diff for images
- File finder is available in the repository viewer
- Blame is displayed at the source viewer
- Remain user data and repositories even if user is disabled
- Mobile view improvement
### 3.2 - 3 May 2015
- Directory history button
- Compare / pull request button
- Limit of activity log
### 3.1.1 - 4 Apr 2015
- Rolled back H2 version to avoid version compatibility issue
- Plug-ins became possible to access ServletContext
### 3.1 - 28 Mar 2015
- Web APIs for Jenkins github pull-request builder
- Improved diff view
@@ -270,3 +299,7 @@ Release Notes
### 1.0 - 04 Jul 2013
- This is a first public release
Sponsors
--------
[![IntelliJ IDEA](https://www.jetbrains.com/idea/docs/logo_intellij_idea.png)](https://www.jetbrains.com/idea/)

View File

@@ -6,7 +6,7 @@ This directory has following structure:
```
* /HOME/gitbucket
* /repositoties
* /repositories
* /USER_NAME
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
* / REPO_NAME

View File

@@ -16,6 +16,8 @@ for Developers
--------
If you want to modify source code and confirm it, you can run GitBucket in auto reloading mode as following:
Windows:
```
C:\gitbucket> sbt
...
@@ -24,15 +26,38 @@ C:\gitbucket> sbt
> ~ ;copy-resources;aux-compile
```
Linux:
```
~/gitbucket$ ./sbt.sh
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Build war file
--------
To build war file, run the following command:
Windows:
```
C:\gitbucket> sbt package
```
Linux:
```
~/gitbucket$ ./sbt.sh package
```
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
To build executable war file, run Ant at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact. Please note the current build.xml works on Windows only. Replace `sbt.bat` with `sbt.sh` in build.xml if you want to run it on Linux.
To build executable war file, run
* Windows: Not available
* Linux: `./release/make-release-war.sh`
at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact.

View File

@@ -379,21 +379,21 @@
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
</g>
<g id="rect3075-11">
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill="#FFFFFF"/>
<path d="M779.562,898.094 C779.396,912.75 779.229,927.406 779.062,942.062 C781.26,942.084 783.458,942.104 785.656,942.125 C784.104,943.688 782.552,945.25 781,946.813 C801.708,967.521 822.417,988.229 843.125,1008.938 C858.531,993.531 873.938,978.125 889.344,962.719 C868.635,942 847.927,921.281 827.219,900.563 C825.49,902.302 823.76,904.042 822.031,905.781 C822.052,903.386 822.073,900.99 822.094,898.594 C807.917,898.427 793.74,898.261 779.563,898.094 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill="#FFFFFF"/>
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
</g>
<path d="M606.483,964.91 L606.483,951.243 L672.089,951.243 L672.089,964.91 z" fill="#B3B3B3" id="rect2995-0-2-8-6"/>
<g id="rect3075-11-7">
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill="#FFFFFF"/>
<path d="M786.383,905.075 C786.256,916.229 786.129,927.383 786.003,938.537 C787.675,938.553 789.348,938.568 791.021,938.584 C789.839,939.773 788.658,940.963 787.477,942.152 C803.237,957.911 818.997,973.671 834.756,989.431 C846.481,977.706 858.206,965.982 869.93,954.257 C854.171,938.489 838.411,922.722 822.651,906.954 C821.335,908.278 820.019,909.602 818.703,910.926 C818.719,909.102 818.734,907.279 818.751,905.456 C807.961,905.329 797.172,905.202 786.383,905.075 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill="#FFFFFF"/>
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
</g>
<g id="path3100-2">
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill="#FFFFFF"/>
<path d="M813.748,916.688 C818.255,921.195 818.255,928.501 813.748,933.008 C809.242,937.514 801.935,937.514 797.429,933.008 C792.922,928.501 792.922,921.195 797.429,916.688 C801.935,912.182 809.242,912.182 813.748,916.688 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill="#FFFFFF"/>
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
</g>
<g id="rect4114">
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill="#FFFFFF"/>
<path d="M813.845,955.107 L834.888,934.064 L864.012,963.188 L842.969,984.231 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill="#FFFFFF"/>
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
</g>
<g id="path2991-7-6">
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill="#A0A0A0"/>
@@ -750,5 +750,45 @@
</g>
<path d="M1396.792,592.168 C1426.908,592.168 1450.613,610.989 1450.613,639.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
<path d="M1397.792,545.653 C1453.613,544.493 1499.627,588.735 1499.627,636.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
<path d="M871.125,1039.025 C871.125,1039.025 873.794,1016.889 908.043,1011.524 C919.748,1009.691 945.861,1005.107 945.861,978.522" fill-opacity="0" stroke="#A0A0A0" stroke-width="17.059" id="path3207"/>
<g id="rect3818-4-8-4">
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
<g>
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="15"/>
</g>
</g>
<g id="path3795-4-8-7-8">
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill="#FFFFFF"/>
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8">
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill="#FFFFFF"/>
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2">
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill="#FFFFFF"/>
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<path d="M1114.353,1042.412 C1114.353,1042.412 1117.022,1020.275 1151.271,1014.91 C1162.976,1013.077 1189.089,1008.493 1189.089,981.909" fill-opacity="0" stroke="#000000" stroke-width="17.059" id="path3207"/>
<g id="rect3818-4-8-4">
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
<g>
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill-opacity="0" stroke="#000000" stroke-width="15"/>
</g>
</g>
<g id="path3795-4-8-7-8">
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill="#FFFFFF"/>
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8">
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill="#FFFFFF"/>
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2">
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill="#FFFFFF"/>
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -8,3 +8,4 @@ Developer's Guide
* [Activity Types](activity.md)
* [Notification Email](notification.md)
* [Automatic Schema Updating](auto_update.md)
* [Release Operation](release.md)

54
doc/release.md Normal file
View File

@@ -0,0 +1,54 @@
Release Operation
========
Update version number
--------
Note to update version number in files below:
### project/build.scala
```scala
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.3.0" // <---- update version!!
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
```
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
```scala
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 3), // <---- add this line!!
new Version(3, 2),
```
Generate release files
--------
Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/).
### Make release war file
Run `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`.
```bash
$ cd release
$ ./make-release-war.sh
```
### Deploy assembly jar file
For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`.
```bash
$ cd release/
$ ./deploy-assembly-jar.sh
```

3
env.sh Normal file
View File

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

14
gitbucket-assembly.iml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -10,7 +10,7 @@ import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.0.0"
val Version = "3.5.0"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
@@ -50,21 +50,22 @@ object MyBuild extends Build {
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1", // 1.4.2 has incompatible APi changes
"org.pegdown" % "pegdown" % "1.5.0",
"org.apache.commons" % "commons-compress" % "1.9",
"org.apache.commons" % "commons-email" % "1.3.3",
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.186",
"com.h2database" % "h2" % "1.4.180",
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.12" % "test",
"com.mchange" % "c3p0" % "0.9.5",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.play" %% "twirl-compiler" % "1.0.4"
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
),
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
EclipseKeys.withSource := true,

View File

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir=".">
<project name="gitbucket" default="all" basedir="..">
<property environment="env"/>
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="3.0.0"/>
<property name="gitbucket.version" value="${env.GITBUCKET_VERSION}"/>
<property name="jetty.version" value="8.1.16.v20140903"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>

View File

@@ -1,9 +1,15 @@
#!/bin/sh
. ../env.sh
cd ../
./sbt.sh clean assembly
cd release
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=3.0.0\
-Dversion=$GITBUCKET_VERSION\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-3.0.0.jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/

15
release/make-release-war.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
D="$(dirname "$0")"
D="$(cd "${D}"; pwd)"
DD="$(dirname "${D}")"
(
for f in "${DD}/env.sh" "${D}/build.xml"; do
if [ ! -s "${f}" ]; then
echo >&2 "$0: Unable to access file '${f}'"
exit 1
fi
done
cd "${DD}"
. "${DD}/env.sh"
ant -f "${D}/build.xml" all
)

View File

@@ -1,7 +1,7 @@
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, TransactionFilter}
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
import gitbucket.core.util.Directory
import java.util.EnumSet
@@ -47,4 +47,8 @@ class ScalatraBootstrap extends LifeCycle {
dir.mkdirs()
}
}
override def destroy(context: ServletContext): Unit = {
Database.closeDataSource()
}
}

View File

@@ -1,6 +1,7 @@
package gitbucket.core.api
import gitbucket.core.model.IssueComment
import gitbucket.core.util.RepositoryName
import java.util.Date
@@ -13,14 +14,16 @@ case class ApiComment(
user: ApiUser,
body: String,
created_at: Date,
updated_at: Date)
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
}
object ApiComment{
def apply(comment: IssueComment, user: ApiUser): ApiComment =
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser): ApiComment =
ApiComment(
id = comment.commentId,
user = user,
body = comment.content,
created_at = comment.registeredDate,
updated_at = comment.updatedDate)
updated_at = comment.updatedDate)(repositoryName, issueId)
}

View File

@@ -1,6 +1,7 @@
package gitbucket.core.api
import gitbucket.core.model.Issue
import gitbucket.core.util.RepositoryName
import java.util.Date
@@ -16,10 +17,13 @@ case class ApiIssue(
state: String,
created_at: Date,
updated_at: Date,
body: String)
body: String)(repositoryName: RepositoryName){
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}")
}
object ApiIssue{
def apply(issue: Issue, user: ApiUser): ApiIssue =
def apply(issue: Issue, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
ApiIssue(
number = issue.issueId,
title = issue.title,
@@ -27,5 +31,5 @@ object ApiIssue{
state = if(issue.closed){ "closed" }else{ "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate,
updated_at = issue.updatedDate)
updated_at = issue.updatedDate)(repositoryName)
}

View File

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

View File

@@ -23,7 +23,7 @@ object JsonFormat {
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
FieldSerializer[ApiCommitStatus]() + FieldSerializer[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]()
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>

View File

@@ -91,7 +91,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"name" -> trim(label("Repository name", text(required, maxlength(40), repository, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
@@ -202,15 +202,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName")
getAccountByUserName(userName, true).foreach { account =>
// 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)
// // 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(isRemoved = true))
}
@@ -467,6 +467,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
parentUserName = Some(repository.owner)
)
// Add collaborators for group repository
val ownerAccount = getAccountByUserName(accountName).get
if(ownerAccount.isGroupAccount){
getGroupMembers(accountName).foreach { member =>
addCollaborator(accountName, repository.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(accountName, repository.name)

View File

@@ -99,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray)
)
})

View File

@@ -86,7 +86,7 @@ trait IssuesControllerBase extends ControllerBase {
issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield {
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, ApiUser(user)) })
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) })
}).getOrElse(NotFound)
})
@@ -132,11 +132,11 @@ trait IssuesControllerBase extends ControllerBase {
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
}
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
redirect(s"/${owner}/${name}/issues/${issueId}")
@@ -190,7 +190,7 @@ trait IssuesControllerBase extends ControllerBase {
(issue, id) <- handleComment(issueId, Some(body), repository)()
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, ApiUser(context.loginAccount.get)))
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get)))
}) getOrElse NotFound
})
@@ -348,9 +348,12 @@ trait IssuesControllerBase extends ControllerBase {
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){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
fromIssue.issueId + ":" + fromIssue.title, "refer")
// 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")
}
}
}
}
@@ -418,13 +421,13 @@ trait IssuesControllerBase extends ControllerBase {
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issueId, _){
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, issueId, _){
f.toNotify(repository, issue, _){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}

View File

@@ -125,10 +125,10 @@ trait PullRequestsControllerBase extends ControllerBase {
(for{
issueId <- params("id").toIntOpt
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.userName), Set())
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.userName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
} yield {
JsonFormat(ApiPullRequest(
@@ -236,7 +236,7 @@ trait PullRequestsControllerBase extends ControllerBase {
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier().toNotify(repository, issue, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
@@ -248,6 +248,7 @@ trait PullRequestsControllerBase extends ControllerBase {
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
val headBranch:Option[String] = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
@@ -255,8 +256,8 @@ trait PullRequestsControllerBase extends ControllerBase {
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
@@ -265,7 +266,7 @@ trait PullRequestsControllerBase extends ControllerBase {
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
} getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
@@ -280,12 +281,18 @@ trait PullRequestsControllerBase extends ControllerBase {
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
originRepositoryName <- if(originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
// Original repository
forkedRepository.repository.originRepositoryName
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
// Sibling repository
getUserRepositories(originOwner, context.baseUrl).find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}.map(_.repository.repositoryName)
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
@@ -394,8 +401,10 @@ trait PullRequestsControllerBase extends ControllerBase {
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")

View File

@@ -163,7 +163,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))

View File

@@ -1,6 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
import gitbucket.core.helper
import gitbucket.core.service._
@@ -32,7 +33,6 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService
/**
* The repository viewer.
*/
@@ -284,27 +284,60 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Displays the file content of the specified branch or commit.
*/
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
RawData(FileUtil.getContentType(path, bytes), bytes)
}
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
RawData("application/octet-stream", bytes)
} getOrElse NotFound
} else {
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
html.blob(id, repository, path.split("/").toList,
JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame")
}
} getOrElse NotFound
}
})
get("/:owner/:repository/blame/*"){
blobRoute.action()
}
/**
* Blame data.
*/
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
contentType = formats("json")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
Map(
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
"id" -> id,
"path" -> path,
"last" -> last,
"blame" -> JGitUtil.getBlame(git, id, path).map{ blame =>
Map(
"id" -> blame.id,
"author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
"avatar" -> view.helpers.avatarLink(blame.authorName, 32, blame.authorEmailAddress).toString,
"authed" -> helper.html.datetimeago(blame.authorTime).toString,
"prev" -> blame.prev,
"prevPath" -> blame.prevPath,
"commited" -> blame.commitTime.getTime,
"message" -> blame.message,
"lines" -> blame.lines)
})
}
})
/**
* Displays details of the specified commit.
*/
@@ -403,10 +436,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.reverse
val branches = JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
@@ -475,6 +514,34 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository)
})
/**
* Displays the file find of branch.
*/
get("/:owner/:repository/find/:ref")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
html.find(params("ref"),
treeId,
repository,
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
})
} getOrElse NotFound
}
})
/**
* Get all file list of branch.
*/
ajaxGet("/:owner/:repository/tree-list/:tree")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val treeId = params("tree")
contentType = formats("json")
Map("paths" -> JGitUtil.getAllFileListByTreeId(git, treeId))
}
})
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
@@ -486,7 +553,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
s"readme.${extension}"
} ++ Seq("readme.txt", "readme")
/**
* Provides HTML of the file list.
@@ -525,6 +594,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound

View File

@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(

View File

@@ -0,0 +1,42 @@
package gitbucket.core.plugin
import gitbucket.core.model.Session
import gitbucket.core.service.SystemSettingsService.SystemSettings
/**
* Define the Git repository routing.
*
* @param urlPattern the regular expression which matches the repository path (e.g. "gist/(.+?)/(.+?)\\.git")
* @param localPath the string to assemble local file path of repository (e.g. "gist/$1/$2")
* @param filter the filter for request to the Git repository which is defined by this routing
*/
case class GitRepositoryRouting(urlPattern: String, localPath: String, filter: GitRepositoryFilter){
def this(urlPattern: String, localPath: String) = {
this(urlPattern, localPath, new GitRepositoryFilter(){
def filter(repositoryName: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
(implicit session: Session): Boolean = true
})
}
}
/**
* Filters request to plug-in served repository. This is used to provide authentication mainly.
*/
trait GitRepositoryFilter {
/**
* Filters request to Git repository. If this method returns true then request is accepted.
*
* @param path the repository path which starts with '/'
* @param userName the authenticated user name or None
* @param settings the system settings
* @param isUpdating true if update request, otherwise false
* @param session the database session
* @return true if allow accessing to repository, otherwise false.
*/
def filter(path: String, userName: Option[String], settings: SystemSettings, isUpdating: Boolean)
(implicit session: Session): Boolean
}

View File

@@ -1,5 +1,9 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Version
/**
@@ -13,16 +17,92 @@ trait Plugin {
val description: String
val versions: Seq[Version]
/**
* Override to declare this plug-in provides images.
*/
val images: Seq[(String, Array[Byte])] = Nil
/**
* Override to declare this plug-in provides images.
*/
def images(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Array[Byte])] = Nil
/**
* Override to declare this plug-in provides controllers.
*/
val controllers: Seq[(String, ControllerBase)] = Nil
/**
* Override to declare this plug-in provides controllers.
*/
def controllers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, ControllerBase)] = Nil
/**
* Override to declare this plug-in provides JavaScript.
*/
val javaScripts: Seq[(String, String)] = Nil
/**
* Override to declare this plug-in provides JavaScript.
*/
def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = Nil
/**
* Override to declare this plug-in provides renderers.
*/
val renderers: Seq[(String, Renderer)] = Nil
/**
* Override to declare this plug-in provides renderers.
*/
def renderers(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, Renderer)] = Nil
/**
* Override to add git repository routings.
*/
val repositoryRoutings: Seq[GitRepositoryRouting] = Nil
/**
* Override to add git repository routings.
*/
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
*/
def initialize(registry: PluginRegistry): Unit
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {
(images ++ images(registry, context, settings)).foreach { case (id, in) =>
registry.addImage(id, in)
}
(controllers ++ controllers(registry, context, settings)).foreach { case (path, controller) =>
registry.addController(path, controller)
}
(javaScripts ++ javaScripts(registry, context, settings)).foreach { case (path, script) =>
registry.addJavaScript(path, script)
}
(renderers ++ renderers(registry, context, settings)).foreach { case (extension, renderer) =>
registry.addRenderer(extension, renderer)
}
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
registry.addRepositoryRouting(routing)
}
}
/**
* This method is invoked in shutdown of plugin system.
* If the plugin has any resources, release them in this method.
*/
def shutdown(registry: PluginRegistry): Unit
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
/**
* Helper method to get a resource from classpath.
*/
protected def fromClassPath(path: String): Array[Byte] =
using(getClass.getClassLoader.getResourceAsStream(path)){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
}

View File

@@ -7,6 +7,7 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._
@@ -23,6 +24,11 @@ class PluginRegistry {
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
private val renderers = mutable.Map[String, Renderer]()
renderers ++= Seq(
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
@@ -30,34 +36,68 @@ class PluginRegistry {
def getPlugins(): List[PluginInfo] = plugins.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
images += ((id, encoded))
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
images += ((id, encoded))
addImage(id, bytes)
}
def getImage(id: String): String = images(id)
def addController(controller: ControllerBase, path: String): Unit = {
def addController(path: String, controller: ControllerBase): Unit = {
controllers += ((controller, path))
}
def getControllers(): List[(ControllerBase, String)] = controllers.toList
def addJavaScript(path: String, script: String): Unit = {
javaScripts += Tuple2(path, script)
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = {
addController(path, controller)
}
//def getJavaScripts(): List[(String, String)] = javaScripts.toList
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
def addJavaScript(path: String, script: String): Unit = {
javaScripts += ((path, script))
}
def getJavaScript(currentPath: String): List[String] = {
javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
}
def addRenderer(extension: String, renderer: Renderer): Unit = {
renderers += ((extension, renderer))
}
def getRenderer(extension: String): Renderer = {
renderers.get(extension).getOrElse(DefaultRenderer)
}
def renderableExtensions: Seq[String] = renderers.keys.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = {
repositoryRoutings += routing
}
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = {
repositoryRoutings.toSeq
}
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
case GitRepositoryRouting(urlPath, _, _) => {
repositoryPath.matches("/" + urlPath + "(/.*)?")
}
}
}
private case class GlobalAction(
method: String,
path: String,
@@ -89,7 +129,7 @@ object PluginRegistry {
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
@@ -119,7 +159,7 @@ object PluginRegistry {
}
// Initialize
plugin.initialize(instance)
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
@@ -137,10 +177,10 @@ object PluginRegistry {
}
}
def shutdown(context: ServletContext): Unit = {
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
instance.getPlugins().foreach { pluginInfo =>
try {
pluginInfo.pluginClass.shutdown(instance)
pluginInfo.pluginClass.shutdown(instance, context, settings)
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)

View File

@@ -0,0 +1,45 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService
import gitbucket.core.view.Markdown
import play.twirl.api.Html
/**
* A render engine to render content to HTML.
*/
trait Renderer {
/**
* Render the given request to HTML.
*/
def render(request: RenderRequest): Html
}
object MarkdownRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(Markdown.toHtml(fileContent, repository, enableWikiLink, enableRefsLink, enableAnchor)(context))
}
}
object DefaultRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
case class RenderRequest(filePath: List[String],
fileContent: String,
branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
context: Context)

View File

@@ -7,6 +7,12 @@ import profile.simple._
trait ActivityService {
def deleteOldActivities(limit: Int)(implicit s: Session): Int = {
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
Activities.filter(_.activityId <= id.bind).delete
} getOrElse 0
}
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))

View File

@@ -22,10 +22,11 @@ trait IssuesService {
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
IssueComments filter (_.byIssue(owner, repository, issueId)) list
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session) =
/** @return IssueComment and commentedUser */
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
IssueComments.filter(_.byIssue(owner, repository, issueId))
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
.innerJoin(Accounts).on( (t1, t2) => t1.userName === t2.userName )
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
.list
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
@@ -168,7 +169,7 @@ trait IssuesService {
}
/** for api
* @return (issue, commentCount, pullRequest, headRepository, headOwner)
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
@@ -176,7 +177,7 @@ trait IssuesService {
searchIssueQueryBase(condition, true, offset, limit, repos)
.innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.userName }
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName }
.innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName }
.map { case (((((t1, t2), t3), t4), t5), t6) =>
(t1, t5, t2.commentCount, t3, t4, t6)

View File

@@ -83,6 +83,28 @@ trait PullRequestService { self: IssuesService =>
.map { case (t1, t2) => t1 }
.list
/**
* for repository viewer.
* 1. find pull request from from `branch` to othre branch on same repository
* 1. return if exists pull request to `defaultBranch`
* 2. return if exists pull request to othre branch
* 2. return None
*/
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
(implicit s: Session): Option[(PullRequest, Issue)] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === branch.bind) &&
(t1.userName === userName.bind) &&
(t1.repositoryName === repositoryName.bind) &&
(t2.closed === false.bind)
}
.sortBy{ case (t1, t2) => t1.branch =!= defaultBranch.bind }
.firstOption
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/

View File

@@ -19,6 +19,7 @@ trait SystemSettingsService {
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString)
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
if(settings.notification) {
@@ -65,12 +66,13 @@ trait SystemSettingsService {
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getOptionValue(props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false),
getOptionValue(props, SshPort, Some(DefaultSshPort)),
if(getValue(props, Notification, false)){
@@ -120,6 +122,7 @@ object SystemSettingsService {
isCreateRepoOptionPublic: Boolean,
gravatar: Boolean,
notification: Boolean,
activityLogLimit: Option[Int],
ssh: Boolean,
sshPort: Option[Int],
smtp: Option[Smtp],
@@ -166,6 +169,7 @@ object SystemSettingsService {
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh"
private val SshPort = "ssh.port"
private val SmtpHost = "smtp.host"

View File

@@ -50,6 +50,7 @@ trait WebHookService {
val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", eventName)
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
@@ -80,16 +81,16 @@ trait WebHookPullRequestService extends WebHookService {
// https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issues"){
val users = getAccountsByUserNames(Set(repository.owner, issue.userName), Set(sender))
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{
repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.userName)
issueUser <- users.get(issue.openedUserName)
} yield {
WebHookIssuesPayload(
action = action,
number = issue.issueId,
repository = ApiRepository(repository, ApiUser(repoOwner)),
issue = ApiIssue(issue, ApiUser(issueUser)),
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
sender = ApiUser(sender))
}
}
@@ -100,14 +101,16 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, "pull_request"){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName), Set(sender))
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -118,8 +121,9 @@ trait WebHookPullRequestService extends WebHookService {
}
}
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
(implicit s: Session): Map[(Issue, PullRequest, Account, Account), List[WebHook]] =
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] =
(for{
is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
@@ -128,20 +132,22 @@ trait WebHookPullRequestService extends WebHookService {
if pr.requestBranch === branch.bind
bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
} yield {
((is, pr, bu, ru), wh)
((is, iu, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2))
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
for{
((issue, 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)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -161,10 +167,10 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
callWebHookOf(repository.owner, repository.name, "issue_comment"){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(Set(issue.userName, repository.owner, issueComment.userName), Set(sender))
issueUser <- users.get(issue.userName)
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
issueUser <- users.get(issue.openedUserName)
repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.userName)
commenter <- users.get(issueComment.commentedUserName)
} yield {
WebHookIssueCommentPayload(
issue = issue,
@@ -224,6 +230,7 @@ object WebHookService {
object WebHookPullRequestPayload{
def apply(action: String,
issue: Issue,
issueUser: Account,
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -233,7 +240,7 @@ object WebHookService {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, senderPayload)
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser))
WebHookPullRequestPayload(
action = action,
number = issue.issueId,
@@ -265,8 +272,8 @@ object WebHookService {
WebHookIssueCommentPayload(
action = "created",
repository = ApiRepository(repository, repositoryUser),
issue = ApiIssue(issue, ApiUser(issueUser)),
comment = ApiComment(comment, ApiUser(commentUser)),
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)),
sender = ApiUser(sender))
}
}

View File

@@ -33,7 +33,7 @@ class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
case None => chain.doFilter(req, res)
case Some(Left(_)) => {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
response.setContentType("Content-Type: application/json; charset=utf-8")
response.setContentType("application/json; charset=utf-8")
val w = response.getWriter()
w.print("""{ "message": "Bad credentials" }""")
w.close()

View File

@@ -0,0 +1,170 @@
package gitbucket.core.servlet
import java.io.File
import java.sql.{DriverManager, Connection}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util._
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 5),
new Version(3, 4),
new Version(3, 3),
new Version(3, 2),
new Version(3, 1),
new Version(3, 0),
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
ignore {
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
}
}
}
}
},
Version(1, 13),
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0),
Version(0, 0)
)
/**
* The head version of BitBucket.
*/
val headVersion = versions.head
/**
* The version file (GITBUCKET_HOME/version).
*/
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
}.getOrElse(Version(0, 0))
}
case _ => Version(0, 0)
}
} else Version(0, 0)
}
}

View File

@@ -2,11 +2,12 @@ package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http._
import gitbucket.core.plugin.{GitRepositoryFilter, GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
import gitbucket.core.util.{ControlUtil, Keys, Implicits}
import gitbucket.core.util.{Keys, Implicits}
import org.slf4j.LoggerFactory
import Implicits._
import ControlUtil._
/**
* Provides BASIC Authentication for [[GitRepositoryServlet]].
@@ -20,7 +21,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
implicit val request = req.asInstanceOf[HttpServletRequest]
val request = req.asInstanceOf[HttpServletRequest]
val response = res.asInstanceOf[HttpServletResponse]
val wrappedResponse = new HttpServletResponseWrapper(response){
@@ -31,43 +32,13 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
val settings = loadSystemSettings()
try {
defining(request.paths){
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(req, wrappedResponse)
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case Array(username, password) => {
authenticate(settings, username, password) match {
case Some(account) if (isUpdating || repository.repository.isPrivate) => {
if(hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
} else {
requireAuth(response)
}
}
case _ => requireAuth(response)
}
}
case _ => requireAuth(response)
}
}
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
case _ => {
logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
PluginRegistry().getRepositoryRouting(request.gitRepositoryPath).map { case GitRepositoryRouting(_, _, filter) =>
// served by plug-ins
pluginRepository(request, wrappedResponse, chain, settings, isUpdating, filter)
}.getOrElse {
// default repositories
defaultRepository(request, wrappedResponse, chain, settings, isUpdating)
}
} catch {
case ex: Exception => {
@@ -77,6 +48,67 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
}
private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = {
implicit val r = request
val account = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield {
request.setAttribute(Keys.Request.UserName, account.userName)
account
}
if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
chain.doFilter(request, response)
} else {
requireAuth(response)
}
}
private def defaultRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
settings: SystemSettings, isUpdating: Boolean): Unit = {
implicit val r = request
request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(request, response)
} else {
val passed = for {
auth <- Option(request.getHeader("Authorization"))
Array(username, password) = decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password)
} yield if(isUpdating || repository.repository.isPrivate){
if(hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
true
} else false
} else true
if(passed.getOrElse(false)){
chain.doFilter(request, response)
} else {
requireAuth(response)
}
}
}
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
case _ => {
logger.debug(s"Not enough path arguments: ${request.paths}")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
}
}
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -1,7 +1,10 @@
package gitbucket.core.servlet
import java.io.File
import gitbucket.core.api
import gitbucket.core.model.Session
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
import gitbucket.core.service._
@@ -18,7 +21,6 @@ import org.eclipse.jgit.transport.resolver._
import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
@@ -35,20 +37,8 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory())
// TODO are there any other ways...?
super.init(new ServletConfig(){
def getInitParameter(name: String): String = name match {
case "base-path" => Directory.RepositoryHome
case "export-all" => "true"
case name => config.getInitParameter(name)
}
def getInitParameterNames(): java.util.Enumeration[String] = {
config.getInitParameterNames
}
def getServletContext(): ServletContext = config.getServletContext
def getServletName(): String = config.getServletName
})
val root: File = new File(Directory.RepositoryHome)
setRepositoryResolver(new GitBucketRepositoryResolver(new FileResolver[HttpServletRequest](root, true)))
super.init(config)
}
@@ -67,32 +57,52 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
}
}
class GitBucketRepositoryResolver(parent: FileResolver[HttpServletRequest]) extends RepositoryResolver[HttpServletRequest] {
private val resolver = new FileResolver[HttpServletRequest](new File(Directory.GitBucketHome), true)
override def open(req: HttpServletRequest, name: String): Repository = {
// Rewrite repository path if routing is marched
PluginRegistry().getRepositoryRouting("/" + name).map { case GitRepositoryRouting(urlPattern, localPath, _) =>
val path = urlPattern.r.replaceFirstIn(name, localPath)
resolver.open(req, path)
}.getOrElse {
parent.open(req, name)
}
}
}
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("pusher:" + pusher)
if(PluginRegistry().getRepositoryRouting(request.gitRepositoryPath).isEmpty){
val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
defining(request.paths){ paths =>
val owner = paths(1)
val repository = paths(2).stripSuffix(".git")
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("pusher:" + pusher)
logger.debug("repository:" + owner + "/" + repository)
defining(request.paths){ paths =>
val owner = paths(1)
val repository = paths(2).stripSuffix(".git")
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
logger.debug("repository:" + owner + "/" + repository)
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
}
}
receivePack
}
receivePack
}
}

View File

@@ -1,176 +1,22 @@
package gitbucket.core.servlet
import java.io.File
import java.sql.{DriverManager, Connection}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util._
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
import gitbucket.core.plugin._
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 1),
new Version(3, 0),
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
ignore {
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
}
}
}
}
},
Version(1, 13),
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0),
Version(0, 0)
)
/**
* The head version of BitBucket.
*/
val headVersion = versions.head
/**
* The version file (GITBUCKET_HOME/version).
*/
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
}.getOrElse(Version(0, 0))
}
case _ => Version(0, 0)
}
} else Version(0, 0)
}
}
import akka.actor.{Actor, Props, ActorSystem}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import AutoUpdate._
/**
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
*/
class InitializeListener extends ServletContextListener {
import AutoUpdate._
class InitializeListener extends ServletContextListener with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
@@ -181,28 +27,62 @@ class InitializeListener extends ServletContextListener {
}
org.h2.Driver.load()
defining(getConnection()){ conn =>
Database() withTransaction { session =>
val conn = session.conn
// Migration
logger.debug("Start schema update")
Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
}
// Load plugins
logger.debug("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, conn)
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
}
// Start Quartz scheduler
val system = ActorSystem("job", ConfigFactory.parseString(
"""
|akka {
| quartz {
| schedules {
| Daily {
| expression = "0 0 0 * * ?"
| }
| }
| }
|}
""".stripMargin))
val scheduler = QuartzSchedulerExtension(system)
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
def contextDestroyed(event: ServletContextEvent): Unit = {
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown plugins
PluginRegistry.shutdown(event.getServletContext)
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
// Close datasource
Database.closeDataSource()
}
private def getConnection(): Connection =
DriverManager.getConnection(
DatabaseConfig.url,
DatabaseConfig.user,
DatabaseConfig.password)
}
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
private val logger = Logging(context.system, this)
def receive = {
case s: String => {
loadSystemSettings().activityLogLimit.foreach { limit =>
if(limit > 0){
Database() withTransaction { implicit session =>
val rows = deleteOldActivities(limit)
logger.info(s"Deleted ${rows} activity logs")
}
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ import javax.servlet._
import javax.servlet.http.HttpServletRequest
import com.mchange.v2.c3p0.ComboPooledDataSource
import gitbucket.core.util.DatabaseConfig
import org.scalatra.ScalatraBase
import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session}
import gitbucket.core.util.Keys
@@ -25,6 +26,12 @@ class TransactionFilter extends Filter {
chain.doFilter(req, res)
} else {
Database() withTransaction { session =>
// Register Scalatra error callback to rollback transaction
ScalatraBase.onFailure { _ =>
logger.debug("Rolled back transaction")
session.rollback()
}(req.asInstanceOf[HttpServletRequest])
logger.debug("begin transaction")
req.setAttribute(Keys.Request.DBSession, session)
chain.doFilter(req, res)
@@ -39,17 +46,18 @@ object Database {
private val logger = LoggerFactory.getLogger(Database.getClass)
private val db: SlickDatabase = {
val datasource = new ComboPooledDataSource
datasource.setDriverClass(DatabaseConfig.driver)
datasource.setJdbcUrl(DatabaseConfig.url)
datasource.setUser(DatabaseConfig.user)
datasource.setPassword(DatabaseConfig.password)
private val dataSource: ComboPooledDataSource = {
val ds = new ComboPooledDataSource
ds.setDriverClass(DatabaseConfig.driver)
ds.setJdbcUrl(DatabaseConfig.url)
ds.setUser(DatabaseConfig.user)
ds.setPassword(DatabaseConfig.password)
logger.debug("load database connection pool")
ds
}
SlickDatabase.forDataSource(datasource)
private val db: SlickDatabase = {
SlickDatabase.forDataSource(dataSource)
}
def apply(): SlickDatabase = db
@@ -57,4 +65,6 @@ object Database {
def getSession(req: ServletRequest): Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session]
def closeDataSource(): Unit = dataSource.close
}

View File

@@ -1,12 +1,13 @@
package gitbucket.core.ssh
import gitbucket.core.model.Session
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
import gitbucket.core.servlet.{Database, CommitLogHook}
import gitbucket.core.util.{Directory, ControlUtil}
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
import org.slf4j.LoggerFactory
import java.io.{InputStream, OutputStream}
import java.io.{File, InputStream, OutputStream}
import ControlUtil._
import org.eclipse.jgit.api.Git
import Directory._
@@ -15,11 +16,11 @@ import org.apache.sshd.server.command.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand {
val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
}
abstract class GitCommand(val owner: String, val repoName: String) extends Command {
self: RepositoryService with AccountService =>
abstract class GitCommand() extends Command {
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
@@ -71,6 +72,11 @@ abstract class GitCommand(val owner: String, val repoName: String) extends Comma
this.in = in
}
}
abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand {
self: RepositoryService with AccountService =>
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean =
getAccountByUserName(username) match {
@@ -80,7 +86,8 @@ abstract class GitCommand(val owner: String, val repoName: String) extends Comma
}
class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -94,11 +101,10 @@ class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends Gi
}
}
}
}
class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName)
with SystemSettingsService with RepositoryService with AccountService {
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
@@ -116,18 +122,56 @@ class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends G
}
}
}
}
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), false)){
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
val repository = git.getRepository
val upload = new UploadPack(repository)
upload.upload(in, out, err)
}
}
}
}
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
if(routing.filter.filter("/" + repoName, Some(user), loadSystemSettings(), true)){
val path = routing.urlPattern.r.replaceFirstIn(repoName, routing.localPath)
using(Git.open(new File(Directory.GitBucketHome, path))){ git =>
val repository = git.getRepository
val receive = new ReceivePack(repository)
receive.receive(in, out, err)
}
}
}
}
class GitCommandFactory(baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")
command match {
case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(owner, repoName, baseUrl)
case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(owner, repoName, baseUrl)
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case _ => new UnknownCommand(command)
}
}
private def pluginRepository(repoName: String): Boolean = PluginRegistry().getRepositoryRouting("/" + repoName).isDefined
private def routing(repoName: String): GitRepositoryRouting = PluginRegistry().getRepositoryRouting("/" + repoName).get
}

View File

@@ -63,15 +63,17 @@ object Implicits {
implicit class RichRequest(request: HttpServletRequest){
def paths: Array[String] = (request.getRequestURI match{
case path if path.startsWith("/api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
case path => path
}).substring(request.getContextPath.length + 1).split("/")
}).split("/")
def hasQueryString: Boolean = request.getQueryString != null
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/")
}
implicit class RichSession(session: HttpSession){

View File

@@ -100,7 +100,8 @@ object JGitUtil {
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
/**
* The file content data for the file content view of the repository viewer.
@@ -138,6 +139,9 @@ object JGitUtil {
case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String)
case class BlameInfo(id: String, authorName: String, authorEmailAddress: String, authorTime:java.util.Date,
prev: Option[String], prevPath: Option[String], commitTime:java.util.Date, message:String, lines:Set[Int])
/**
* Returns RevCommit from the commit or tag id.
*
@@ -196,80 +200,121 @@ object JGitUtil {
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId==null) return Nil
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = if (path == ".") {
def useTreeWalk(rev:RevCommit)(f:TreeWalk => Any): Unit = if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree)
treeWalk
treeWalk.addTree(rev.getTree)
using(treeWalk)(f)
} else {
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
val treeWalk = TreeWalk.forPath(git.getRepository, path, rev.getTree)
if(treeWalk != null){
treeWalk.enterSubtree
using(treeWalk)(f)
}
}
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, Option[String], RevCommit)): (ObjectId, FileMode, String, Option[String], RevCommit) = tuple match {
case (oid, FileMode.TREE, name, _, commit ) =>
(using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(oid)
// single tree child, or None
if(walk.next() && walk.getFileMode(0) == FileMode.TREE){
Some((walk.getObjectId(0), walk.getFileMode(0), name + "/" + walk.getNameString, None, commit)).filterNot(_ => walk.next())
} else {
None
}
}) match {
case Some(child) => simplifyPath(child)
case _ => tuple
}
case _ => tuple
}
using(treeWalk) { treeWalk =>
def tupleAdd(tuple:(ObjectId, FileMode, String, Option[String]), rev:RevCommit) = tuple match {
case (oid, fmode, name, opt) => (oid, fmode, name, opt, rev)
}
@tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, Option[String]), Map[RevCommit, RevCommit])],
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, Option[String], RevCommit)] ={
if(restList.isEmpty){
result
}else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
result ++ restList.map{ case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) }
}else{
val newCommit = revIterator.next
val (thisTimeChecks,skips) = restList.partition{ case (tuple, parentsMap) => parentsMap.contains(newCommit) }
if(thisTimeChecks.isEmpty){
findLastCommits(result, restList, revIterator)
}else{
var nextRest = skips
var nextResult = result
// Map[(name, oid), (tuple, parentsMap)]
val rest = scala.collection.mutable.Map(thisTimeChecks.map{ t => (t._1._3 -> t._1._1) -> t }:_*)
lazy val newParentsMap = newCommit.getParents.map(_ -> newCommit).toMap
useTreeWalk(newCommit){ walk =>
while(walk.next){
rest.remove(walk.getNameString -> walk.getObjectId(0)).map{ case (tuple, _) =>
if(newParentsMap.isEmpty){
nextResult +:= tupleAdd(tuple, newCommit)
}else{
nextRest +:= tuple -> newParentsMap
}
}
}
}
rest.values.map{ case (tuple, parentsMap) =>
val restParentsMap = parentsMap - newCommit
if(restParentsMap.isEmpty){
nextResult +:= tupleAdd(tuple, parentsMap(newCommit))
}else{
nextRest +:= tuple -> restParentsMap
}
}
findLastCommits(nextResult, nextRest, revIterator)
}
}
}
var fileList: List[(ObjectId, FileMode, String, Option[String])] = Nil
useTreeWalk(revCommit){ treeWalk =>
while (treeWalk.next()) {
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
val linkUrl =if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, linkUrl)
}
list.transform(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
simplifyPath(tuple)
)
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(tuple._1)
while (walk.next() && list.size < 2) {
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
} else None
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
}
}
revWalk.markStart(revCommit)
val it = revWalk.iterator
val lastCommit = it.next
val nextParentsMap = Option(lastCommit).map(_.getParents.map(_ -> lastCommit).toMap).getOrElse(Map())
findLastCommits(List.empty, fileList.map(a => a -> nextParentsMap), it)
.map(simplifyPath)
.map { case (objectId, fileMode, name, linkUrl, commit) =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getAuthorIdent.getWhen,
commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
}
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
tuple
else
simplifyPath(list(0))
}
}
}.toList
}
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name, linkUrl) =>
defining(commits(path)){ commit =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getAuthorIdent.getWhen,
commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
}
}.toList
}
/**
@@ -283,6 +328,39 @@ object JGitUtil {
}
}
/**
* get all file list by revision. only file.
*/
def getTreeId(git: Git, revision: String): Option[String] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId==null) return None
val revCommit = revWalk.parseCommit(objectId)
Some(revCommit.getTree.name)
}
}
/**
* get all file list by tree object id.
*/
def getAllFileListByTreeId(git: Git, treeId: String): List[String] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(treeId+"^{tree}")
if(objectId==null) return Nil
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(objectId)
treeWalk.setRecursive(true)
var ret: List[String] = Nil
if(treeWalk != null){
while (treeWalk.next()) {
ret +:= treeWalk.getPathString
}
}
ret.reverse
}
}
}
/**
* Returns the commit list of the specified branch.
*
@@ -313,12 +391,7 @@ object JGitUtil {
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF))
}
Right(getCommitLog(revWalk.iterator, 0, Nil))
}
@@ -420,11 +493,13 @@ object JGitUtil {
treeWalk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(treeWalk.next){
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
}))
}
(buffer.toList, None)
@@ -444,12 +519,15 @@ object JGitUtil {
import scala.collection.JavaConverters._
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
if(!fetchContent || oldIsImage || newIsImage){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
}
}.toList
}
@@ -603,21 +681,24 @@ object JGitUtil {
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(objectId)
val large = FileUtil.isLarge(loader.getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
// binary
ContentInfo("binary", None, None)
}
} else {
// binary
ContentInfo("binary", None, None)
// image or large
ContentInfo(viewer, None, None)
}
} else {
// image or large
ContentInfo(viewer, None, None)
}
}
@@ -630,12 +711,12 @@ object JGitUtil {
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
Some(loader.getBytes)
}
}
} catch {
@@ -710,26 +791,31 @@ object JGitUtil {
return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
}
def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = {
def getBranches(owner: String, name: String, defaultBranch: String, origin: Boolean): Seq[BranchInfo] = {
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val defaultObject = repo.resolve(defaultBranch)
val defaultObject = if (repo.getAllRefs.keySet().contains(defaultBranch)) {
repo.resolve(defaultBranch)
} else {
git.branchList().call().iterator().next().getObjectId
}
git.branchList.call.asScala.map { ref =>
val walk = new RevWalk(repo)
try{
try {
val defaultCommit = walk.parseCommit(defaultObject)
val branchName = ref.getName.stripPrefix("refs/heads/")
val branchCommit = if(branchName == defaultBranch){
defaultCommit
}else{
} else {
walk.parseCommit(ref.getObjectId)
}
val when = branchCommit.getCommitterIdent.getWhen
val committer = branchCommit.getCommitterIdent.getName
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
val mergeInfo = if(branchName==defaultBranch){
val mergeInfo = if(origin && branchName == defaultBranch){
None
}else{
} else {
walk.reset()
walk.setRevFilter( RevFilter.MERGE_BASE )
walk.markStart(branchCommit)
@@ -750,6 +836,36 @@ object JGitUtil {
}
}
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId =>
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo(
c.name,
c.getAuthorIdent.getName,
c.getAuthorIdent.getEmailAddress,
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty)
}
idLine :+= (c.name, i)
}
val limeMap = idLine.groupBy(_._1).mapValues(_.map(_._2).toSet)
blameMap.values.map{b => b.copy(lines=limeMap(b.id))}
}.getOrElse(Seq.empty)
}
/**
* Returns sha1
* @param owner repository owner

View File

@@ -15,7 +15,7 @@ import SystemSettingsService.Smtp
import ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
@@ -67,16 +67,15 @@ object Notifier {
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context) = {
val database = Database()
val f = Future {
database withSession { implicit session =>
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining(
s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
defining(
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
msg(Markdown.toHtml(content, r, false, true, false))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)
@@ -88,18 +87,17 @@ class Mailer(private val smtp: Smtp) extends Notifier {
email.setSSLOnConnect(ssl)
}
smtp.fromAddress
.map (_ -> smtp.fromName.orNull)
.map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName))
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
}
}
}
"Notifications Successful."
@@ -113,6 +111,6 @@ class Mailer(private val smtp: Smtp) extends Notifier {
}
}
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}

View File

@@ -6,7 +6,7 @@ import org.scalatra.i18n.Messages
trait Validations {
/**
* Constraint for the identifier such as user name, repository name or page name.
* Constraint for the identifier such as user name or page name.
*/
def identifier: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
@@ -19,6 +19,23 @@ trait Validations {
}
}
/**
* Constraint for the repository identifier.
*/
def repository: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("[a-zA-Z0-9\\-\\+_.]+")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
/**
* Constraint for the color pattern.
*/
def color = pattern("#[0-9a-fA-F]{6}")
/**

View File

@@ -2,7 +2,6 @@ package gitbucket.core.view
import gitbucket.core.controller.Context
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.Implicits
import gitbucket.core.util.Implicits.RichString
trait LinkConverter { self: RequestCache =>
@@ -11,10 +10,12 @@ trait LinkConverter { self: RequestCache =>
* Converts issue id, username and commit id to link.
*/
protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo,
issueIdPrefix: String = "#")(implicit context: Context): String = {
value
// escape HTML tags
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
issueIdPrefix: String = "#", escapeHtml: Boolean = true)(implicit context: Context): String = {
// escape HTML tags
val escaped = if(escapeHtml) value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") else value
escaped
// convert issue id to link
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
getIssue(repository.owner, repository.name, m.group(2)) match {

View File

@@ -18,11 +18,20 @@ object Markdown {
/**
* Converts Markdown of Wiki pages to HTML.
*
* @param repository the repository which contains the markdown
* @param enableWikiLink if true then wiki style link is available in markdown
* @param enableRefsLink if true then issue reference (e.g. #123) is rendered as link
* @param enableAnchor if true then anchor for headline is generated
* @param enableTaskList if true then task list syntax is available
* @param hasWritePermission
* @param pages the list of existing Wiki pages
*/
def toHtml(markdown: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): String = {
@@ -38,10 +47,14 @@ object Markdown {
} else s
val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS |
Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML | Extensions.STRIKETHROUGH
).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
new GitBucketHtmlSerializer(
markdown, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList,
hasWritePermission, pages
).toHtml(rootNode)
}
}
@@ -100,6 +113,7 @@ class GitBucketHtmlSerializer(
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean,
pages: List[String]
@@ -108,9 +122,9 @@ class GitBucketHtmlSerializer(
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
) with LinkConverter with RequestCache {
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
.print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
override protected def printImageTag(rendering: LinkRenderer.Rendering): Unit = {
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(rendering.href, true)).print("\">")
.print("<img src=\"").print(fixUrl(rendering.href, true)).print("\" alt=\"").printEncoded(rendering.text).print("\"/></a>")
}
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
@@ -123,8 +137,10 @@ class GitBucketHtmlSerializer(
}
private def fixUrl(url: String, isImage: Boolean = false): String = {
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
url
} else if(url.startsWith("#")){
("#" + GitBucketHtmlSerializer.generateAnchorName(url.substring(1)))
} else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "")
@@ -148,15 +164,34 @@ class GitBucketHtmlSerializer(
private def printHeaderTag(node: HeaderNode): Unit = {
val tag = s"h${node.getLevel}"
val headerTextString = printChildrenToString(node)
val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString)
val child = node.getChildren.asScala.headOption
val anchorName = child match {
case Some(x: AnchorLinkNode) => x.getName
case Some(x: TextNode) => x.getText
case _ => GitBucketHtmlSerializer.generateAnchorName(extractText(node)) // TODO
}
printer.print(s"""<$tag class="markdown-head">""")
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
visitChildren(node)
if(enableAnchor){
printer.print(s"""<a class="markdown-anchor-link" href="#$anchorName"></a>""")
printer.print(s"""<a class="markdown-anchor" name="$anchorName"></a>""")
}
child match {
case Some(x: AnchorLinkNode) => printer.print(x.getText)
case _ => visitChildren(node)
}
printer.print(s"</$tag>")
}
private def extractText(node: Node): String = {
val sb = new StringBuilder()
node.getChildren.asScala.map {
case x: TextNode => sb.append(x.getText)
case x: Node => sb.append(extractText(x))
}
sb.toString()
}
override def visit(node: HeaderNode): Unit = {
printHeaderTag(node)
}
@@ -175,6 +210,18 @@ class GitBucketHtmlSerializer(
}
}
override def visit(node: VerbatimNode) {
val printer = new Printer()
val serializer = verbatimSerializers.get(VerbatimSerializer.DEFAULT)
serializer.serialize(node, printer)
val html = printer.getString
// convert commit id and username to link.
val t = if(enableRefsLink) convertRefsLinks(html, repository, "issue:", escapeHtml = false) else html
this.printer.print(t)
}
override def visit(node: BulletListNode): Unit = {
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
printer.println().print("""<ul class="task-list">""").indent(+2)

View File

@@ -5,14 +5,12 @@ import java.util.{Date, Locale, TimeZone}
import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState
import gitbucket.core.plugin.{RenderRequest, PluginRegistry}
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{JGitUtil, StringUtil}
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
import play.twirl.api.Html
/**
* Provides helper methods for Twirl templates.
*/
@@ -83,14 +81,6 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def plural(count: Int, singular: String, plural: String = ""): String =
if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural
private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, RepositoryService.RepositoryInfo, Boolean, Boolean, Context) => Html)] =
Seq(
".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)),
".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context))
)
def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1)
/**
* Converts Markdown of Wiki pages to HTML.
*/
@@ -101,29 +91,28 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, true, hasWritePermission, pages))
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: Context): Html = {
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
val fileNameLower = filePath.reverse.head.toLowerCase
renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match {
case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context)
case None => Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
val fileName = filePath.reverse.head.toLowerCase
val extension = FileUtil.getExtension(fileName)
val renderer = PluginRegistry().getRenderer(extension)
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
}
def isRenderable(fileName: String): Boolean = {
PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
}
/**
* Returns &lt;img&gt; which displays the avatar icon for the given user name.
* This method looks up Gravatar if avatar icon has not been configured in user settings.
*/
def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: Context): Html =
getAvatarImageHtml(userName, size, "", tooltip)
def avatar(userName: String, size: Int, tooltip: Boolean = false, mailAddress: String = "")(implicit context: Context): Html =
getAvatarImageHtml(userName, size, mailAddress, tooltip)
/**
* Returns &lt;img&gt; which displays the avatar icon for the given mail address.
@@ -148,7 +137,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
import scala.util.matching.Regex._
implicit class RegexReplaceString(s: String) {
def replaceAll(pattern: String, replacer: (Match) => String): String = {
pattern.r.replaceAllIn(s, replacer)
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
}
}
@@ -166,6 +155,11 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
)
/**
* Remove html tags from the given Html instance.
*/
def removeHtml(html: Html): Html = Html(html.body.replaceAll("<.+?>", ""))
/**
* URL encode except '/'.
*/
@@ -203,7 +197,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* If user does not exist or disabled, this method returns avatar image without link.
*/
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html =
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip))
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html =
(if(mailAddress.isEmpty){

View File

@@ -21,7 +21,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
<span class="slash">/</span>
<input type="text" name="name" id="name" />
<input type="text" name="name" id="name" autofocus />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
@@ -31,7 +31,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_public.png"/>&nbsp;</i>&nbsp;Public</span><br>
<span class="strong"><i class="octicon octicon-repo"></i>&nbsp;</i>&nbsp;Public</span><br>
<div>
<span>All users and guests can read this repository.</span>
</div>
@@ -40,7 +40,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<fieldset>
<label class="radio">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><img src="@assets/common/images/repo_private.png"/>&nbsp;</i>&nbsp;Private</span><br>
<span class="strong"><i class="icon-lock"></i>&nbsp;</i>&nbsp;Private</span><br>
<div>
<span>Only collaborators can read this repository.</span>
</div>

View File

@@ -9,7 +9,7 @@
<div class="span6">
<fieldset>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<input type="text" name="userName" id="userName" value="" autofocus/>
<span id="error-userName" class="error"></span>
</fieldset>
<fieldset>

View File

@@ -81,6 +81,15 @@
</label>
</fieldset>
<!--====================================================================-->
<!-- Activity -->
<!--====================================================================-->
<hr>
<label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label>
<div class="controls">
<input type="text" id="activityLogLimit" name="activityLogLimit" class="input-mini" value="@settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span>
</div>
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
<hr>

View File

@@ -6,11 +6,11 @@
@import gitbucket.core.view.helpers._
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
@closedCount Closed
</a>
</span>

View File

@@ -19,9 +19,9 @@
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
}
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){
@@ -39,18 +39,19 @@
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i>
@milestone</a></span>
}
</div>
</td>

View File

@@ -4,16 +4,16 @@
<div class="dashboard-nav">
<div class="container">
<a href="@path/" @if(active == ""){ class="active"}>
<img src="@assets/common/images/menu-feed.png">
<i class="octicon octicon-rss"></i>
News Feed
</a>
@if(loginAccount.isDefined){
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
<img src="@assets/common/images/menu-pulls.png">
<i class="octicon octicon-git-pull-request"></i>
Pull Requests
</a>
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
<img src="@assets/common/images/menu-issues.png">
<i class="octicon octicon-issue-opened"></i>
Issues
</a>
}
@@ -27,6 +27,8 @@ div.dashboard-nav {
margin-bottom: 20px;
}
div.dashboard-nav a {
line-height: 10px;
margin-left: 20px;
@@ -36,8 +38,17 @@ div.dashboard-nav a {
color: #888;
}
div.dashboard-nav a:hover {
div.dashboard-nav .octicon{
width: 16px;
height: 16px;
font-size: 16px;
color: #888;
}
div.dashboard-nav a:hover,div.dashboard-nav a:hover .octicon {
text-decoration: none;
color: #333;
}
div.dashboard-nav a.active {

View File

@@ -8,20 +8,20 @@
@activities.map { activity =>
<div class="block">
@(activity.activityType match {
case "open_issue" => detailActivity(activity, "activity-issue.png")
case "comment_issue" => detailActivity(activity, "activity-comment.png")
case "comment_commit" => detailActivity(activity, "activity-comment.png")
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "delete_branch" => simpleActivity(activity, "activity-delete.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "delete_tag" => simpleActivity(activity, "activity-delete.png")
case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){
case "open_issue" => detailActivity(activity, "issue-opened")
case "comment_issue" => detailActivity(activity, "comment-discussion")
case "comment_commit" => detailActivity(activity, "comment-discussion")
case "close_issue" => detailActivity(activity, "issue-closed")
case "reopen_issue" => detailActivity(activity, "issue-reopened")
case "open_pullreq" => detailActivity(activity, "git-pull-request")
case "merge_pullreq" => detailActivity(activity, "git-merge")
case "create_repository" => simpleActivity(activity, "repo")
case "create_branch" => simpleActivity(activity, "git-branch")
case "delete_branch" => simpleActivity(activity, "circle-slash")
case "create_tag" => simpleActivity(activity, "tag")
case "delete_tag" => simpleActivity(activity, "circle-slash")
case "fork" => simpleActivity(activity, "repo-forked")
case "push" => customActivity(activity, "git-commit"){
<div class="small activity-message">
{activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
if(i == 3){
@@ -37,12 +37,12 @@
}}
</div>
}
case "create_wiki" => customActivity(activity, "activity-wiki.png"){
case "create_wiki" => customActivity(activity, "book"){
<div class="small activity-message">
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div>
}
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
case "edit_wiki" => customActivity(activity, "book"){
activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) =>
<div class="small activity-message">
@@ -61,7 +61,7 @@
}
@detailActivity(activity: gitbucket.core.model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
<div class="activity-content">
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@@ -75,7 +75,7 @@
}
@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-icon-large"><i class="mega-octicon octicon-@image"></i></div>
<div class="activity-content">
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
<div class="strong">
@@ -87,7 +87,7 @@
}
@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = {
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
<div class="activity-icon-small"><i class="octicon octicon-@image"></i></div>
<div class="activity-content">
<div>
@avatar(activity.activityUserName, 16)

View File

@@ -25,16 +25,16 @@
<span class="pull-right diffstat" data-diff-id="@i"></span>
<a href="#diff-@i">
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
<i class="octicon octicon-diff-renamed"></i> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/> @diff.newPath
<i class="octicon octicon-diff-added"></i> @diff.newPath
}
@if(diff.changeType == ChangeType.MODIFY){
<img src="@assets/common/images/diff_edit.png"/> @diff.newPath
<i class="octicon octicon-diff-modified"></i> @diff.newPath
}
@if(diff.changeType == ChangeType.DELETE){
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
<i class="octicon octicon-diff-removed"></i> @diff.oldPath
}
</a>
</li>
@@ -55,7 +55,7 @@
</div>
}
<span class="diffstat">
<img src="@assets/common/images/diff_move.png"/>
<i class="octicon octicon-diff-renamed"></i>
</span> @diff.oldPath -> @diff.newPath
}
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@@ -68,9 +68,9 @@
}
<span class="diffstat">
@if(diff.changeType == ChangeType.ADD){
<img src="@assets/common/images/diff_add.png"/>
<i class="octicon octicon-diff-added"></i>
}else{
<img src="@assets/common/images/diff_edit.png"/>
<i class="octicon octicon-diff-modified"></i>
}
</span>
@diff.newPath
@@ -83,20 +83,39 @@
</div>
}
<span class="diffstat">
<img src="@assets/common/images/diff_delete.png"/>
<i class="octicon octicon-diff-removed"></i>
</span> @diff.oldPath
}
</th>
</tr>
<tr>
<td style="padding: 0;">
@if(diff.newContent != None || diff.oldContent != None){
@if(diff.oldObjectId == diff.newObjectId){
<div class="diff-same">File renamed without changes</div>
} else { @if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div>
<textarea id="newText-@i" style="display: none;" data-file-name="@diff.oldPath">@diff.newContent.getOrElse("")</textarea>
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
} else { @if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@url(repository)/blob/@oldCommitId.get/@diff.oldPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.ADD){
Not supported
}
}
@if(newCommitId.isDefined && diff.newIsImage){
<div class="diff-image-frame diff-new"><img src="@url(repository)/blob/@newCommitId.get/@diff.newPath?raw=true" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {
@if(diff.changeType != ChangeType.DELETE){
Not supported
}
}
</div>
} else {
Not supported
}
} } }
</td>
</tr>
</table>
@@ -160,7 +179,7 @@ $(function(){
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
}
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$('.inline-comment').hide();
$('#comment-list').children('.inline-comment').hide();
}
$('.diff-outside').on('click','table.diff .add-comment',function() {
var $this = $(this),

View File

@@ -16,7 +16,7 @@
<published>@datetimeRFC3339(activity.activityDate)</published>
<updated>@datetimeRFC3339(activity.activityDate)</updated>
<link type="text/html" rel="alternate" href="@context.baseUrl/@activity.userName/@activity.repositoryName" />
<title type="html">@activity.activityType</title>
<title type="html">@removeHtml(activityMessage(activity.message))</title>
<author>
<name>@activity.activityUserName</name>
<uri>@url(activity.activityUserName)</uri>

View File

@@ -5,6 +5,7 @@
enableTaskList: Boolean,
hasWritePermission: Boolean,
style: String = "",
styleClass: String = "",
placeholder: String = "Leave a comment",
elastic: Boolean = false,
uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context)
@@ -20,7 +21,9 @@
<div class="tab-pane active" id="tab@uid">
<span id="error-content" class="error"></span>
@textarea = {
<textarea id="content@uid" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
<textarea id="content@uid" name="content" placeholder="@placeholder"
@if(style.nonEmpty){ style="@style"}
@if(styleClass.nonEmpty){ class="@styleClass" }>@content</textarea>
}
@if(enableWikiLink){
@textarea

View File

@@ -3,11 +3,11 @@
@import context._
@import gitbucket.core.view.helpers._
@if(repository.repository.isPrivate){
<img src="@assets/common/images/repo_private@{if(large){"_lg"}}.png"/>
<i class="@{if(large){"mega-"}}octicon octicon-lock"></i>
} else {
@if(repository.repository.originUserName.isDefined){
<img src="@assets/common/images/repo_fork@{if(large){"_lg"}}.png"/>
<i class="@{if(large){"mega-"}}octicon octicon-repo-forked"></i>
} else {
<img src="@assets/common/images/repo_public@{if(large){"_lg"}}.png"/>
<i class="@{if(large){"mega-"}}octicon octicon-repo"></i>
}
}

View File

@@ -42,6 +42,13 @@ div#clickable {
line-height: 120px;
}
div.dz-message, div.dz-fallback {
width: 240px;
color: #000000;
font-size: 12px;
line-height: 14px;
}
div#avatar {
background-color: #f8f8f8;
border: 1px dashed silver;

View File

@@ -10,7 +10,16 @@
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box">
<div class="box-content">
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 635px; height: 100px; max-height: 150px;",
elastic = true
)
</div>
</div>
<div class="pull-right">

View File

@@ -13,7 +13,7 @@
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil"></i></a>
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil" aria-label="Edit"></i></a>
}
</span>
</div>
@@ -41,8 +41,8 @@
<span class="pull-right">
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle" aria-label="Remove"></i></a>
}
</span>
</div>

View File

@@ -16,7 +16,7 @@
<div class="box issue-box">
<div class="box-content">
<span id="error-title" class="error"></span>
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;"/>
<input type="text" name="title" value="" placeholder="Title" style="width: 565px;" autofocus/>
<div>
<span id="label-assigned">No one is assigned</span>
@if(hasWritePermission){
@@ -41,7 +41,7 @@
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><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 {
<span class="muted">Due by @date(dueDate)</span>
}
@@ -57,7 +57,16 @@
</div>
</div>
<hr>
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 565px; height: 200px; max-height: 250px;",
elastic = true
)
</div>
</div>
<div class="pull-right">

View File

@@ -42,7 +42,7 @@
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><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 {
<span class="muted">Due by @date(dueDate)</span>
}

View File

@@ -11,7 +11,7 @@
<div style="margin-top: 6px">
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<img src="@assets/common/images/label_@(if(label.fontColor == "ffffff") "white" else "black").png" style="width: 12px;"/>
<i class="octicon octicon-tag" style="color: #@label.fontColor;"></i>
@label.labelName
</span>
</a>

View File

@@ -16,8 +16,7 @@
@if(condition.nonEmpty){
<div>
<a href="@gitbucket.core.service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<img src="@assets/common/images/clear.png" class="header-icon"/>
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
<i class="octicon octicon-x" ></i>
<span class="strong">Clear current search query, filters, and sorts</span>
</a>
</div>
@@ -28,11 +27,11 @@
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
@openCount Open
</a>&nbsp;&nbsp;
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
@closedCount Closed
</a>
</span>
@@ -176,7 +175,7 @@
@if(hasWritePermission){
<input type="checkbox" value="@issue.issueId"/>
}
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png" style="margin-right: 20px;"/>
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")" style="margin-right: 3px;"></i>
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@@ -195,18 +194,19 @@
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
<i class="octicon octicon-comment active"></i> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
<i class="octicon octicon-comment"></i> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i>
@milestone</a></span>
}
</div>
</td>
@@ -216,4 +216,3 @@
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), gitbucket.core.service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

View File

@@ -13,11 +13,11 @@
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
<img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
<i class="octicon octicon-milestone @(if(state == "open"){"active"})"></i>
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
<i class="octicon octicon-milestone @(if(state == "closed"){"active"})"></i>
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
@@ -38,7 +38,7 @@
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><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 {
<span class="muted">Due by @date(dueDate)</span>
}

View File

@@ -8,21 +8,18 @@
<head>
<meta charset="utf-8">
<title>@title</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Le styles -->
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="@assets/vendors/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="@assets/vendors/bootstrap/js/html5shiv.js"></script>
<![endif]-->
<link href="@assets/vendors/octicons/octicons.css" rel="stylesheet">
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
<script src="@assets/vendors/jquery/jquery-1.11.1.js"></script>
<script src="@assets/vendors/dropzone/dropzone.js"></script>
<script src="@assets/common/js/validation.js"></script>
<script src="@assets/common/js/gitbucket.js"></script>
@@ -33,12 +30,18 @@
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
<script src="@assets/vendors/facebox/facebox.js"></script>
<script src="@assets/vendors/jquery-hotkeys/jquery.hotkeys.js"></script>
@repository.map { repository =>
@if(!repository.repository.isPrivate){
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
}
}
</head>
<body>
<form id="search" action="@path/search" method="POST">
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<div class="container" style="width: 920px;">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@@ -49,27 +52,38 @@
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
</a>
@repository.map { repository =>
<input type="text" name="query" style="width: 200px; margin-top: 5px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
@if(loginAccount.isDefined){
<a href="@path/dashboard/pulls" class="global-header-menu">Pull requests</a>
<a href="@path/dashboard/issues" class="global-header-menu">Issues</a>
}
<div class="nav-collapse collapse pull-right header-menu">
@repository.map { repository =>
<input type="text" name="query" style="width: 300px; margin-bottom: 0px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
@if(loginAccount.isDefined){
<a href="@url(loginAccount.get.userName)" class="username menu">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</a>
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="icon-plus"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
@if(loginAccount.get.isAdmin){
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
}
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
<div class="btn-group" style="margin-top: 0px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#"><i class="octicon octicon-plus" style="font-size: 20px; vertical-align: top; margin-top: 10px;"></i><span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
</div>
<div class="btn-group" style="margin-top: 0px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">@avatar(loginAccount.get.userName, 16)<span class="caret" style="vertical-align: middle;"></span></a>
<ul class="dropdown-menu">
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
@if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">System administration</a></li>
}
<li><a href="@path/signout">Sign out</a></li>
</ul>
</div>
} else {
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn" id="signin">Sign in</a>
}
</div><!--/.nav-collapse -->
</div>

View File

@@ -9,16 +9,11 @@
@import context._
@import gitbucket.core.view.helpers._
@sidemenu(path: String, name: String, label: String, count: Int = 0) = {
@sidemenu(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"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path">
@if(active == name){
<img src="@assets/common/images/menu-@{name}-active.png">
} else {
<img src="@assets/common/images/menu-@{name}-active.png" class="menu-icon-active" style="display:none;">
<img src="@assets/common/images/menu-@{name}.png" class="menu-icon">
}
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i>
@if(expand){ @label}
@if(expand && count > 0){
<div class="pull-right"><span class="label">@count</span></div>
@@ -27,13 +22,6 @@
</li>
}
@sidemenuPlugin(path: String, name: String, label: String, icon: String) = {
<li @if(active == name){class="active"}>
<div class="@if(active == name){margin} else {gradient} pull-left"></div>
<a href="@url(repository)@path"><img src="@icon"/>@if(expand){ @label}</a>
</li>
}
<div class="container">
@helper.html.information(info)
@helper.html.error(error)
@@ -41,7 +29,7 @@
<div class="pull-right">
<div class="input-prepend">
@if(loginAccount.isEmpty){
<a title="You must be signed in to fork a repository" href="@path/signin" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
<a title="You must be signed in to fork a repository" href="@path/signin?redirect=@urlEncode(s"${path}/${repository.owner}/${repository.name}")" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
} else {
@if(isNoGroup) {
<a id="fork-link" href="javascript:void(0);" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
@@ -51,12 +39,12 @@
}
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
@if(loginAccount.isDefined && isNoGroup){
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork">
<input type="hidden" name="account" value="@loginAccount.get.userName"/>
</form>
}
</div>
@if(loginAccount.isDefined && isNoGroup){
<form id="fork-form" method="post" action="@path/@repository.owner/@repository.name/fork">
<input type="hidden" name="account" value="@loginAccount.get.userName"/>
</form>
}
}
<div class="head">
@helper.html.repositoryicon(repository, true)
@@ -76,12 +64,12 @@
<div style="width: @if(expand){170px} else {40px};" class="pull-right">
<ul class="sidemenu">
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li>
@sidemenu("" , "code" , "Code")
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
@sidemenu("/wiki" , "wiki" , "Wiki")
@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", "Settings")
@sidemenu("/settings" , "settings" , "tools", "Settings")
}
<li style="height: 12px"><div class="gradient pull-left" style="height: 12px"></div></li>
</ul>
@@ -99,10 +87,10 @@
}
@id.map { id =>
<div style="margin-top: 10px;">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;"><i class="icon-download-alt"></i>Download ZIP</a>
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn btn-small" style="width: 147px;font-weight: bold;"><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-small" style="width: 147px;"><i class="icon-download-alt"></i>Download TAR.GZ</a>
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.tar.gz" class="btn btn-small" style="width: 147px;font-weight: bold;"><i class="octicon octicon-cloud-download"></i>Download TAR.GZ</a>
</div>
}
}
@@ -117,22 +105,19 @@
<tr>
<td style="width: 33%; text-align: center;">
<a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link">
<img src="@assets/common/images/header-commits-hover.png" class="header-icon-hover" style="display: none;"/>
<img src="@assets/common/images/header-commits.png" class="header-icon"/>
<i class="octicon octicon-history"></i>
<strong>@repository.commitCount</strong> commits
</a>
</td>
<td style="width: 33%; text-align: center;">
<a href="@url(repository)/branches" class="header-link" class="header-link">
<img src="@assets/common/images/header-branches-hover.png" class="header-icon-hover" style="display: none;"/>
<img src="@assets/common/images/header-branches.png" class="header-icon"/>
<i class="octicon octicon-git-branch"></i>
<strong>@repository.branchList.length</strong> branches
</a>
</td>
<td style="width: 33%; text-align: center;">
<a href="@url(repository)/tags" class="header-link" class="header-link">
<img src="@assets/common/images/header-tags-hover.png" class="header-icon-hover" style="display: none;"/>
<img src="@assets/common/images/header-tags.png" class="header-icon"/>
<i class="octicon octicon-tag"></i>
<strong>@repository.tags.length</strong> releases
</a>
</td>
@@ -145,44 +130,21 @@
</div>
<script>
$(function(){
$('a.header-link').mouseover(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(target).children('strong' ).css('color', '#0088cc');
$(target).children('img.header-icon-hover').css('display', 'inline');
$(target).children('img.header-icon' ).css('display', 'none');
});
$('a.header-link').mouseout(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(target).children('strong' ).css('color', 'black');
$(target).children('img.header-icon-hover').css('display', 'none');
$(target).children('img.header-icon' ).css('display', 'inline');
});
$('ul.sidemenu a').mouseover(function(e){
var target = e.target;
if(e.target.tagName == "IMG"){
if(e.target.tagName == "I"){
target = e.target.parentElement;
}
$(target).prev ('div.gradient' ).css('border-left', '1px solid silver');
$(target).children('img.menu-icon-active').css('display', 'inline');
$(target).children('img.menu-icon' ).css('display', 'none');
});
$('ul.sidemenu a').mouseout(function(e){
var target = e.target;
if(e.target.tagName == "IMG"){
if(e.target.tagName == "I"){
target = e.target.parentElement;
}
$(target).prev ('div.gradient' ).css('border-left', '1px solid #eee');
$(target).children('img.menu-icon-active').css('display', 'none');
$(target).children('img.menu-icon' ).css('display', 'inline');
});
$('a[rel*=facebox]').facebox({

View File

@@ -59,7 +59,15 @@
<div style="width: 600px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 580px" placeholder="Title"/>
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;")
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 580px; height: 200px;"
)
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
@@ -131,7 +139,7 @@ $(function(){
}
@if(hasWritePermission){
function checkConflict(from, to, noConflictHandler, hasConflictHandler){
function checkConflict(from, to){
$('.check-conflict').show();
$.get('@url(repository)/compare/' + from + '...' + to + '/mergecheck',
function(data){ $('.check-conflict').html(data); });

View File

@@ -3,12 +3,32 @@
pathList: List[String],
content: gitbucket.core.util.JGitUtil.ContentInfo,
latestCommit: gitbucket.core.util.JGitUtil.CommitInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
hasWritePermission: Boolean,
isBlame: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){
<div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
</div></div>
<div class="line-age-legend">
<span>Newer</span>
<ol>
<li class="heat1"></li>
<li class="heat2"></li>
<li class="heat3"></li>
<li class="heat4"></li>
<li class="heat5"></li>
<li class="heat6"></li>
<li class="heat7"></li>
<li class="heat8"></li>
<li class="heat9"></li>
<li class="heat10"></li>
</ol>
<span>Older</span>
</div>
@helper.html.branchcontrol(
branch,
repository,
@@ -42,6 +62,9 @@
<a class="btn btn-mini" href="@url(repository)/edit/@encodeRefName(branch)/@pathList.mkString("/")">Edit</a>
}
<a class="btn btn-mini" href="?raw=true">Raw</a>
@if(content.viewType == "text"){
<a class="btn btn-mini blame-action" href="@url(repository)/blame/@latestCommit.id/@pathList.mkString("/")" data-url="@url(repository)/get-blame/@latestCommit.id/@pathList.mkString("/")" data-repository="@url(repository)">Blame</a>
}
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
@if(hasWritePermission){
<a class="btn btn-mini btn-danger" href="@url(repository)/remove/@encodeRefName(branch)/@pathList.mkString("/")">Delete</a>
@@ -52,13 +75,13 @@
<tr>
<td>
@if(content.viewType == "text"){
@defining(pathList.reverse.head) { file =>
@if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) {
@defining(isRenderable(pathList.reverse.head)){ isRrenderable =>
@if(!isBlame && isRrenderable) {
<div class="box-content markdown-body" style="border: none; padding-left: 16px; padding-right: 16px;">
@renderMarkup(pathList, content.content.get, branch, repository, false, false)
@renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
</div>
} else {
<pre class="prettyprint linenums blob">@content.content.get</pre>
<pre class="prettyprint linenums blob @if(!isRrenderable){ no-renderable } ">@content.content.get</pre>
}
}
}
@@ -84,30 +107,111 @@ $(window).load(function(){
updateHighlighting();
}).hashchange();
$('pre.prettyprint ol.linenums li').each(function(i, e){
var pre = $('pre.prettyprint');
pre.append($('<div class="source-line-num">')
.data('line', (i + 1))
.css({
cursor : 'pointer',
var pre = $('pre.prettyprint');
function updateSourceLineNum(){
$('.source-line-num').remove();
var pos = pre.find('ol.linenums').position();
$('<div class="source-line-num">').css({
height:pre.height(),
width:'48px',
cursor:'pointer',
position: 'absolute',
top : $(e).position().top + 'px',
left : pre.position().left + 'px',
width : ($(e).position().left - pre.position().left) + 'px',
height : '16px'
}));
});
$('div.source-line-num').click(function(e){
var line = $(e.target).data('line');
var hash = location.hash;
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
} else {
location.hash = '#L' + line;
top : pos.top + 'px',
left : pos.left + 'px'
}).click(function(e){
$(window).hashchange(function(){})
var pos = $(this).data("pos");
if(!pos){
pos = $('ol.linenums li').map(function(){ return {id:$(this).attr("id"),top:$(this).position().top} }).toArray();
$(this).data("pos",pos);
}
for(var i=0;i<pos.length-1;i++){
if(pos[i+1].top>e.pageY){
break;
}
}
var line = pos[i].id.replace(/^L/,'');
var hash = location.hash;
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
} else {
var p = $("#L"+line).attr('id',"");
location.hash = '#L' + line;
p.attr('id','L'+line);
}
}).appendTo(pre);
}
var repository = $('.blame-action').data('repository');
$('.blame-action').click(function(e){
if(history.pushState && $('pre.prettyprint.no-renderable').length){
e.preventDefault();
history.pushState(null, null, this.href);
updateBlame();
}
});
function updateBlame(){
var m = /^\/(blame|blob)(\/.*)$/.exec(location.href.substring(repository.length));
var mode = m[1];
$('.blame-action').toggleClass("active", mode=='blame').attr('href', repository + (m[1]=='blame'?'/blob':'/blame')+m[2]);
if(pre.parents("td").find(".blame").length){
pre.parents("div.container").toggleClass("blame-container", mode=='blame');
updateSourceLineNum();
return;
}
if(mode=='blob'){
updateSourceLineNum();
return;
}
$(document.body).toggleClass('no-box-shadow',document.body.style.boxShadow===undefined);
$('.blame-action').addClass("active");
var base = $('<div class="blame">').css({height:pre.height()}).prependTo(pre.parents("td")[0]);
base.parents("div.container").addClass("blame-container");
updateSourceLineNum();
$.get($('.blame-action').data('url')).done(function(data){
var blame = data.blame;
var index = [];
for(var i=0;i<blame.length;i++){
for(var j=0;j<blame[i].lines.length;j++){
index[blame[i].lines[j]]=blame[i];
}
}
var blame, lastDiv, now=new Date().getTime();
$('pre.prettyprint ol.linenums li').each(function(i, e){
var p=$(e).position();
var h=$(e).height();
if(blame == index[i]){
lastDiv.css("min-height",(p.top+h+1) - lastDiv.position().top);
}else{
$(e).addClass('blame-sep')
blame = index[i];
var sha = $('<div class="blame-sha">')
.append($('<a>').attr("href",data.root+'/commit/'+blame.id).text(blame.id.substr(0,7)));
if(blame.prev){
sha.append($('<br />'))
.append($('<a class="muted-link">').text('prev').attr("href",data.root+'/blame/'+blame.prev+'/'+(blame.prevPath||data.path)));
}
lastDiv = $('<div class="blame-info">')
.addClass('heat'+Math.min(10,Math.max(1,Math.ceil((now-blame.commited)/(24*3600*1000*70)))))
.toggleClass('blame-last',blame.id==data.last)
.data('line', (i + 1))
.css({
"top" : p.top + 'px',
"min-height" : h+'px'
})
.append(sha)
.append($(blame.avatar).addClass('avatar').css({"float":"left"}))
.append($('<div class="blame-commit-title">').text(blame.message))
.append($('<div class="muted">').html(blame.author+ " authed "+blame.authed))
.appendTo(base);
}
});
});
return false;
};
updateBlame();
});
/**
@@ -120,15 +224,20 @@ function updateHighlighting(){
var lines = hash.substr(1).split('-');
if(lines.length == 1){
$('#' + lines[0]).addClass('highlight');
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
if(!updateHighlighting.scrolling){
$(window).scrollTop($('#' + lines[0]).offset().top - 40);
}
} else if(lines.length > 1){
var start = parseInt(lines[0].substr(1));
var end = parseInt(lines[1].substr(1));
for(var i = start; i <= end; i++){
$('#L' + i).addClass('highlight');
}
$(window).scrollTop($('#L' + start).offset().top - 40);
if(!updateHighlighting.scrolling){
$(window).scrollTop($('#L' + start).offset().top - 40);
}
}
updateHighlighting.scrolling = true;
}
}
</script>

View File

@@ -30,9 +30,17 @@
}
}.getOrElse{
@if(context.loginAccount.isDefined){
<a href="@url(repository)/compare/@{encodeRefName(repository.repository.defaultBranch)}...@{encodeRefName(branch.name)}?expand=1" class="btn btn-small">New Pull Request</a>
<a href="@url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
urlEncode(parent) + ":" + encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
encodeRefName(repository.repository.defaultBranch)
}}...@{encodeRefName(branch.name)}?expand=1" class="btn btn-small">New Pull Request</a>
}else{
<a href="@url(repository)/compare/@{encodeRefName(repository.repository.defaultBranch)}...@{encodeRefName(branch.name)}" class="btn btn-small">Compare</a>
<a href="@url(repository)/compare/@{repository.repository.parentUserName.map { parent =>
urlEncode(parent) + ":" + encodeRefName(repository.repository.defaultBranch)
}.getOrElse {
encodeRefName(repository.repository.defaultBranch)
}}...@{encodeRefName(branch.name)}" class="btn btn-small">Compare</a>
}
}
@if(hasWritePermission){
@@ -75,7 +83,7 @@
<script>
$(function(){
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
var branchName = $(e.target).closest('a').data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
});
$('*[data-toggle=tooltip]').tooltip().css("white-space","nowrap");

View File

@@ -15,7 +15,16 @@
}
<div class="box issue-comment-box">
<div class="box-content">
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
@helper.html.preview(
repository = repository,
content = "",
enableWikiLink = false,
enableRefsLink = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
style = "width: 635px; height: 100px; max-height: 150px;",
elastic = true
)
</div>
@if(fileName.isDefined){
<div class="pull-right" style="margin-top: 10px;">

View File

@@ -25,7 +25,7 @@
<div class="small" style="font-weight: normal;">
@if(branches.nonEmpty){
<span class="muted">
<img src="@assets/common/images/branch.png"/>
<i class="octicon octicon-git-branch"></i>
@branches.zipWithIndex.map { case (branch, i) =>
<a href="@url(repository)/tree/@encodeRefName(branch)" class="branch" id="branch-@i">@branch</a>
}
@@ -33,7 +33,7 @@
}
@if(tags.nonEmpty){
<span class="muted">
<img src="@assets/common/images/tag.png"/>
<i class="octicon octicon-tag"></i>
@tags.zipWithIndex.map { case (tag, i) =>
<a href="@url(repository)/tree/@tag" class="tag" id="tag-@i">@tag</a>
}

View File

@@ -120,7 +120,7 @@ $(function(){
$('#editor').hide();
$('#preview').show()
@if(renderableSuffixes.find(suffix => fileName.map(_.toLowerCase.endsWith(suffix)).getOrElse(false))) {
@if(fileName.map(isRenderable _).getOrElse(false)) {
// update preview
$('#preview').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {

View File

@@ -6,6 +6,7 @@
files: List[gitbucket.core.util.JGitUtil.FileInfo],
readme: Option[(List[String], String)],
hasWritePermission: Boolean,
branchPullRequest: Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)],
info: Option[Any] = None,
error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
@import context._
@@ -13,6 +14,17 @@
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
<div class="head">
<div class="pull-right"><div class="btn-group">
<a href="@url(repository)/find/@encodeRefName(branch)" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" data-hotkey="t" title="Quickly jump between files"><i class="icon icon-th-list"></i></a>
@if(pathList.nonEmpty){
<a href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-mini" data-toggle="tooltip" data-placement="bottom" title="Browse commits for this branch"><i class="icon icon-time"></i></a>
}
</div></div>
@branchPullRequest.map{ case (pullRequest, issue) =>
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-pullrequest-branch btn-mini" title="@issue.title" data-toggle="tooltip">#@pullRequest.issueId</a>
}.getOrElse{
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-success btn-mini"><i class="icon-white icon-retweet" data-toggle="tooltip" title="Compare, review, create a pull request"></i></a>
}
@helper.html.branchcontrol(
branch,
repository,
@@ -27,7 +39,7 @@
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
@if(hasWritePermission){
<a href="@url(repository)/new/@encodeRefName(branch)/@pathList.mkString("/")"><img src="@assets/common/images/newfile.png"/></a>
<a href="@url(repository)/new/@encodeRefName(branch)/@pathList.mkString("/")" title="Create a new file here" style="text-decoration: none;">+</i></a>
}
</div>
<table class="table table-file-list">
@@ -76,12 +88,12 @@
<td width="16">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<img src="@assets/common/images/folder_link.png"/>
<i class="octicon octicon-file-symlink-directory"></i>
} else {
<img src="@assets/common/images/folder.png"/>
<i class="octicon octicon-file-directory"></i>
}
} else {
<img src="@assets/common/images/file.png"/>
<i class="octicon octicon-file-text"></i>
}
</td>
<td>
@@ -116,7 +128,7 @@
@readme.map { case(filePath, content) =>
<div id="readme">
<div class="box-header">@filePath.reverse.head</div>
<div class="box-content markdown-body">@renderMarkup(filePath, content, branch, repository, false, false)</div>
<div class="box-content markdown-body">@renderMarkup(filePath, content, branch, repository, false, false, true)</div>
</div>
}
}

View File

@@ -0,0 +1,121 @@
@(branch: String,
treeId: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
groupNames: List[String]
)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository, Some(branch), false, groupNames.isEmpty){
<div>
<div class="find-input">
<span class="bold"><a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a></span>
/
<input type="text" name="query" autocomplete="off" spellcheck="false" autofocus id="tree-finder-field" />
</div>
</div>
<div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">&times;</button>
You've activated the <em>file finder</em>
by pressing <code>t</code>.
Start typing to filter the file list. Use <code></code> and
<code></code> to navigate,
<code>enter</code> to view files.
</div>
<table id="tree-finder-results" class="table table-file-list" data-url="@url(repository)/tree-list/@treeId">
<tbody class="tree-browser-result-template">
<tr class="tree-browser-result">
<td class="icon"><span class="icon icon-chevron-right"></span></td>
<td class="icon"><img src="@assets/common/images/file.png"/></td>
<td>
<a href="@url(repository)/blob/@encodeRefName(branch)"></a>
</td>
</tr>
</tbody>
<tbody class="no-results" style="display:none">
<tr><th colspan="3">No matching files</th><tr>
</tbody>
</table>
<script>
$(function(){
var paths = [];
var template = $('.tree-browser-result-template tr').clone();
var res = $('.tree-browser-result-template');
var cursor = 0;
var pathBase = template.find("a").attr("href");
var preKeyword;
$.ajax({
url:$('#tree-finder-results').data('url'),
cache: true,
dataType: 'json',
success:function(data){
paths = data.paths;
filter();
}
});
var timer;
$("#tree-finder-field").keydown(function(e){
var target = $(this);
if(e.keyCode == 40){ // DOWN
e.preventDefault();
e.stopPropagation();
changeCursor(cursor+1);
}else if(e.keyCode==38){ // UP
e.preventDefault();
e.stopPropagation();
changeCursor(cursor-1);
}else if(e.keyCode==13){ // ENTER
e.preventDefault();
e.stopPropagation();
target = $(".tree-browser-result.navigation-focus a");
if(target[0]){
target[0].click();
}
}else if(e.keyCode==27){ // ESC
e.preventDefault();
e.stopPropagation();
history.back();
}else{
clearTimeout(timer);
timer=setTimeout(filter,300);
}
});
function changeCursor(newPos){
if(!$(".tree-browser-result")[newPos]){
return $(".tree-browser-result.navigation-focus");
}
$(".tree-browser-result.navigation-focus").removeClass("navigation-focus");
cursor=newPos;
scrollIntoView($($(".tree-browser-result")[cursor]).addClass("navigation-focus"));
}
function filter(){
var v = $('#tree-finder-field').val();
if(v==preKeyword || paths.length==0){
return;
}
scrollIntoView('#tree-finder-field');
preKeyword=v;
cursor=0;
var p = string_score_sort(v, paths, 50);
res.html("");
if(p.length==0){
$(".no-results").show();
return;
}else{
$(".no-results").hide();
for(var i=0;i < p.length;i++){
var row = template.clone();
row.find("a").attr("href",pathBase+"/"+p[i].string).html(string_score_highlight(p[i], '<b>'));
if(cursor==i){
row.addClass("navigation-focus");
}
row.appendTo(res);
}
}
}
});
</script>
}
}

View File

@@ -16,7 +16,7 @@
<form action="@path/signin" method="POST" validate="true">
<label for="userName">Username:</label>
<span id="error-userName" class="error"></span>
<input type="text" name="userName" id="userName" style="width: 95%"/>
<input type="text" name="userName" id="userName" style="width: 95%" autofocus/>
<label for="password">Password:</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" style="width: 95%"/>

View File

@@ -22,7 +22,17 @@
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span>
<input type="text" name="pageName" value="@pageName" style="width: 850px; font-weight: bold;" placeholder="Input a page name."/>
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "")
@helper.html.preview(
repository = repository,
content = page.map(_.content).getOrElse(""),
enableWikiLink = true,
enableRefsLink = false,
enableTaskList = false,
hasWritePermission = false,
style = "width: 850px; height: 400px;",
styleClass = "monospace",
placeholder = ""
)
<input type="text" name="message" value="" style="width: 850px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/>

View File

@@ -36,7 +36,30 @@ h6 {
font-size: 18px;
}
.octicon,.mega-octicon{
color : #999;
width: 14px;
height: 14px;
/*font-size: 14px;*/
text-align: center;
}
.mega-octicon{
width: 32px;
height: 32px;
}
.octicon.active,.mega-octicon.active{
color : #333;
}
.octicon-cloud-download{
color: #333;
margin-right: 5px;
}
.head .octicon,.head .mega-octicon{
color : #BBB;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
@@ -55,6 +78,10 @@ div.header-menu {
line-height: 40px;
}
div.header-menu .octicon{
color: #333;
}
div.header-menu input,
div.header-menu a.btn {
margin-top: 0px;
@@ -73,6 +100,7 @@ div.nav-collapse a.menu-last {
.navbar .brand {
padding-top: 6px;
padding-bottom: 6px;
padding-left: 0px;
}
.navbar .brand img {
@@ -94,12 +122,23 @@ div.input-prepend span.count {
padding-bottom: 6px;
}
a.global-header-menu {
color: black;
font-weight: bold;
font-size: 80%;
margin-left: 10px;
margin-right: 10px;
line-height: 3.5;
}
/*
img.plugin-global-menu {
width: 16px;
height: 16px;
position: relative;
top: -2px;
}
*/
/* ======================================================================== */
/* General Styles */
@@ -230,6 +269,7 @@ div.box-header-small {
background: #f2f8fa;
}
div.box-content {
background-color: white;
border: 1px solid #d8d8d8;
@@ -250,6 +290,10 @@ th.box-header {
text-shadow: 0 1px 0 #fff
}
th.box-header .octicon {
display: inline;
}
th.metal {
background-color: #e0e0e0;
background-image: -moz-linear-gradient(#fafafa, #e8e8e8);
@@ -326,6 +370,10 @@ span.highlight {
width: 100%;
}
.dz-clickable {
cursor: pointer;
}
/****************************************************************************/
/* Side Menu */
/****************************************************************************/
@@ -333,12 +381,6 @@ ul.sidemenu {
margin-left: 0px;
}
ul.sidemenu img {
width: 14px;
height: 14px;
}
ul.sidemenu a:hover{
text-decoration: none;
}
@@ -379,6 +421,9 @@ ul.sidemenu span.badge {
padding-right: 4px;
}
ul.sidemenu a:hover i.menu-icon, ul.sidemenu i.menu-icon-active {
color: #333;
}
/****************************************************************************/
/* Create Repository */
/****************************************************************************/
@@ -545,6 +590,10 @@ div.activity-content {
margin-left: 40px;
}
div[class^="activity-icon"] i{
color: #BBB;
}
/****************************************************************************/
/* Repository Viewer */
/****************************************************************************/
@@ -557,9 +606,8 @@ a.header-link {
font-size: 90%;
}
a.header-link img {
width: 14px;
height: 14px;
a.header-link i.octicon {
opacity: 0.5;
}
a.header-link strong {
@@ -568,10 +616,30 @@ a.header-link strong {
}
a.header-link:hover {
color: #0088cc;
color: #4183c4;
text-decoration: none;
}
a.header-link:hover i.octicon, a.header-link:hover strong{
color: inherit;
}
a.header-link i.octicon-x{
opacity: 1;
width: 20px;
height: 20px;
margin-right: 3px;
color: #FFF;
line-height: 20px;
background-color: #777;
border-radius: 3px;
}
a.header-link:hover i.octicon-x{
background-color: #4183c4;
color: #FFF;
}
table.blobview {
table-layout: fixed;
}
@@ -701,6 +769,12 @@ span.simplified-path {
padding-left: 16px;
}
.btn-pullrequest-branch{
background: none;
border: 1px solid #0088cc;
color: #0088cc;
}
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
@@ -770,6 +844,9 @@ a.button-link {
color: gray;
}
a.button-link i {
color: inherit;
}
a.selected {
font-weight: bold;
color: black;
@@ -785,6 +862,14 @@ table.table-issues {
margin-top: 12px;
}
table.table-issues td .octicon-issue-opened,table.table-issues td .octicon-git-pull-request .open {
color: #6CC644;
}
table.table-issues td .octicon-issue-closed,table.table-issues td .octicon-git-pull-request .closed{
color : #BD2C00;;
}
a.issue-title {
color: #333;
font-weight: bold;
@@ -1145,6 +1230,149 @@ table.diff tbody tr.not-diff:hover td{
white-space: nowrap;
letter-spacing: 0px;
}
.diff-same{
background: #DDD;
color: #BBB;
font-size: 16px;
padding: 20px;
font-weight: bold;
text-align: center;
}
/* ------- for imageDiff */
.diff-image-frame{
display: table-cell;
*float: left; /* for ie7 */
vertical-align: middle;
padding: 20px;
background-color: #eee;
}
.diff-image-frame.diff-old{
padding-right: 2px;
}
.diff-image-frame.diff-new{
padding-left: 2px;
}
.diff-image-frame .diff-meta{
margin-top: 12px;
color: #999;
font-family: Helvetica,arial,freesans,clean,sans-serif;
font-size: 12px;
}
.diff-image-frame.diff-old .diff-meta .diff{
color: #bd2c00;
}
.diff-image-frame.diff-new .diff-meta .diff{
color: #55a532;
}
.diff-image-frame img{
max-height: 410px;
max-width: 410px;
background: url(../images/checker.png);
}
.diff-image-render.diff2up{
width:100%;
text-align: center;
display: table;
}
.diff-image-frame.diff-new img{
border: 1px solid #55a532;
}
.diff-image-frame.diff-old img{
border: 1px solid #bd2c00;
}
.diff-image-stack{
position: relative;
background: #EEE;
padding-top: 20px;
}
.diff-image-stack .diff-old,
.diff-image-stack .diff-new{
position:absolute;
overflow: hidden;
margin:0 20px;
}
.diff-image-stack img {
max-width: none;
background: url(../images/checker.png);
}
.diff-image-stack .diff-new{
border: 1px solid #55a532;
background: #EEE;
}
.diff-image-stack .diff-old{
border: 1px solid #bd2c00;
}
.diff-swipe-handle{
position:absolute;
margin-left: 325px;
left: 100px;
}
.diff-silde-bar{
width: 200px;
position: absolute;
left: 325px;
margin: 6px 0 0 7px;
box-sizing: border-box;
border: 1px solid gray;
height: 8px;
}
.image-diff-tools{
text-align: center;
padding: 4px;
background: #f7f7f7;
}
.image-diff-tools{
font-family: 'Helvetica Neue', Helvetica, arial, freesans, clean, sans-serif;
font-size: 12px;
background: #f7f7f7;
margin: 0;
text-align: center;
}
.image-diff-tools li{
background: none;
display: inline;
cursor: pointer;
border-right: 1px solid #ccc;
padding: 0 5px;
position: relative;
color: #666;
}
.image-diff-tools li:last-child{
border-right: none;
}
.image-diff-tools li.active {
font-weight: bold;
}
.no-canvas .need-canvas{
display: none;
}
.diff-image-stack.swipe .diff-new{
border-right: 1px solid #888;
}
.diff-image-stack.swipe .diff-swipe-handle{
margin-left: 15px;
left: 410px;
}
.diff-image-stack.swipe .diff-silde-bar{
display: none;
}
.diff-image-stack.onion .diff-silde-bar{
background: -ms-linear-gradient(left, #bd2c00 0%,#55a532 100%); /* IE10+ */
background: linear-gradient(to right, #bd2c00 0%,#55a532 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#bd2c00', endColorstr='#55a532',GradientType=1 ); /* IE6-9 */
}
.diff-image-stack.blink .diff-silde-bar{
border-style: dotted;
background-image: linear-gradient(to right, #bd2c00, #bd2c00 50%, #55a532 50%, #55a532 100%);
background-size: 2px 2px;
}
.diff-image-stack.difference {
padding-bottom: 18px;
text-align: center;
}
/****************************************************************************/
/* Repository Settings */
/****************************************************************************/
@@ -1152,7 +1380,7 @@ ul.collaborator {
list-style-type: none;
margin-left: 0px;
}
ul.collaborator li {
background-color: #eee;
border: 1px solid #ccc;
@@ -1160,7 +1388,7 @@ ul.collaborator li {
padding: 6px;
margin-bottom: 2px;
}
ul.collaborator li:hover {
background-color: #f8f8f8;
}
@@ -1219,6 +1447,8 @@ div.markdown-body pre {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
white-space: pre;
word-wrap: normal;
overflow: auto;
}
div.markdown-body code {
@@ -1368,13 +1598,15 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
a.markdown-anchor-link {
position: absolute;
left: -20px;
width: 32px;
height: 16px;
background-image: url(../images/link.png);
background-repeat: no-repeat;
left: -18px;
display: none;
color: #999;
/* From octicon style */
font: normal normal normal 16px/1 octicons;
text-decoration: none;
text-rendering: auto;
}
a.markdown-anchor-link:before { content: '\f05c'} /*  */
h1 a.markdown-anchor-link {
top: 24px;
@@ -1399,3 +1631,235 @@ h5 a.markdown-anchor-link {
h6 a.markdown-anchor-link {
top: 6px;
}
/****************************************************************************/
/* File finder */
/****************************************************************************/
#tree-finder-field{
border: none;
box-shadow: none;
padding: 0;
margin: 0;
vertical-align: baseline;
font-size: 100%;
height: inherit;
width: 780px;
}
.find-input{
font-size: 18px;
margin-bottom: 20px;
}
#tree-finder-results td{
padding:7px 6px;
}
#tree-finder-results td.icon{
width:16px; padding: 7px 2px 7px 6px;
}
#tree-finder-results .tree-browser-result .icon-chevron-right{
visibility: hidden;
}
#tree-finder-results .tree-browser-result.navigation-focus .icon-chevron-right{
visibility: visible;
}
#tree-finder-results .navigation-focus td{
background: #fff;
}
/****************************************************************************/
/* blame */
/****************************************************************************/
.blobview pre.blob{
padding-left: 0;
}
.blobview ol.linenums{
margin-left: 0;
padding-left: 50px;
}
div.container.blame-container{
width:1270px;
}
.line-age-legend {
display: none;
}
.blame-container .line-age-legend {
display: block;
float: right;
font-size: 12px;
color: #777;
}
.blame-container .line-age-legend ol {
display: inline-block;
*display: inline;
*zoom: 1;
list-style: none;
margin: 0 5px;
}
.blame-container .line-age-legend ol li {
display: inline-block;
*display: inline;
*zoom: 1;
width: 8px;
height: 10px;
}
.blame-container pre.blob{
margin-left: 350px;
}
.blame-container pre.prettyprint ol.linenums li.blame-sep{
border-top: 1px solid rgb(219, 219, 219);
margin-top: -1px;
}
.blame-container .hide-if-blame {
display: none;
}
.blame{
font-size: 12px;
white-space: normal;
width: 340px;
float: left;
min-height: 100px;
display: none;
}
.blame-container .blame{
display: block;
}
.blame .blame-commit-title{
font-weight: bold;
color: #333;
line-height: 1.1;
}
.blame .avatar{
margin-right: 4px;
margin-bottom: 4px;
}
.blame .blame-info{
background: white;
box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 0px;
position: absolute;
width: 340px;
padding: 2px;
border-right: 2px solid;
}
.no-box-shadow .blame .blame-info{
border-top: 1px solid #888;
border-bottom: 1px solid #888;
border-left: 1px solid #888;
}
.blame-sha{
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
float: right;
text-align: right;
}
.blame-sha .muted-link{
color: #777;
}
.blame-sha .muted-link:hover{
color: #4183c4;
}
.blame .blame-info:hover{
z-index: 100;
box-shadow:rgba(113, 135, 164, 0.65098) 0px 0px 4px 3px;
}
.blame .blame-info.blame-last{
background: #FDFCED;
}
.blame-info.heat1{ border-right-color:#ffeca7}
.blame-info.heat2{ border-right-color:#ffdd8c}
.blame-info.heat3{ border-right-color:#ffdd7c}
.blame-info.heat4{ border-right-color:#fba447}
.blame-info.heat5{ border-right-color:#f68736}
.blame-info.heat6{ border-right-color:#f37636}
.blame-info.heat7{ border-right-color:#ca6632}
.blame-info.heat8{ border-right-color:#c0513f}
.blame-info.heat9{ border-right-color:#a2503a}
.blame-info.heat10{border-right-color:#793738}
.heat1{background-color:#ffeca7}
.heat2{background-color:#ffdd8c}
.heat3{background-color:#ffdd7c}
.heat4{background-color:#fba447}
.heat5{background-color:#f68736}
.heat6{background-color:#f37636}
.heat7{background-color:#ca6632}
.heat8{background-color:#c0513f}
.heat9{background-color:#a2503a}
.heat10{background-color:#793738}
/****************************************************************************/
/* Mobile */
/****************************************************************************/
@media (max-width: 767px) {
body>form#search {
margin: 0 -20px 20px -20px;
}
body>div.dashboard-nav {
margin: 0 -20px 20px -20px;
padding: 0 10px;
}
.container {
width: auto !important;
}
.nav-pills-group .pull-right #search-filter-box {
width: 90% !important;
position: absolute;
left: 20px;
right: 20px;
margin-top: 42px;
}
.nav-pills-group .pull-right form#search-filter-form {
margin-bottom: 60px !important;
}
.table-issues a.button-link {
width: 42px;
height: 16px;
overflow: hidden;
display: inline-block;
}
.nav-tabs a.btn[href$="/_edit"] {
width: 24px;
white-space: nowrap;
overflow: hidden;
padding: 4px 6px;
margin: 3px 4px 0 0;
}
body>div.container.body {
margin: 0 -12px 40px -12px;
}
.container.body>div[style="width: 170px;"]{
width: 32px !important;
margin-right: -5px;
overflow: hidden;
white-space: nowrap;
}
.container.body>div[style="margin-right: 180px;"]{
margin-right: 32px !important;
}
.container.body>div[style="width: 170px;"] .sidemenu i, .container.body>div[style="width: 170px;"] .sidemenu img {
padding-right: 5px;
}
/* .container.body>div[style="width: 170px;"] .small,.container.body>div[style="width: 170px;"] .input-append, .container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] { */
.container.body>div[style="width: 170px;"] .small,.container.body>div[style="width: 170px;"] .input-append {
display: none;
}
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn{
width: 26px !important;
padding: 2px;
}
.container.body>div[style="width: 170px;"] div[style="margin-top: 10px;"] a.btn i {
margin: 5px 10px 5px 6px;
}
body>.container>#fork-form{
display: inline;
}
}
/****************************************************************************/
/* Print */
/****************************************************************************/
a[href]:after {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

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