Compare commits

...

147 Commits
3.1 ... 3.4

Author SHA1 Message Date
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
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
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
73 changed files with 9087 additions and 6397 deletions

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,27 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 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 +293,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

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 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)

50
doc/release.md Normal file
View File

@@ -0,0 +1,50 @@
Release Operation
========
Update version number
--------
Note to update version number in files below:
### 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),
```
### env.sh
```bash
#!/bin/sh
export GITBUCKET_VERSION=3.3.0 # <---- update here!!
```
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
```

2
env.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
export GITBUCKET_VERSION=3.4.0

View File

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

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 = System.getenv("GITBUCKET_VERSION")
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
@@ -57,14 +57,16 @@ object MyBuild extends Build {
"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.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"
),
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
source ../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/

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

@@ -0,0 +1,3 @@
#!/bin/sh
source ../env.sh
ant -f build.xml all

1
sbt.sh
View File

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

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

@@ -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))
}

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.
*/
@@ -475,6 +508,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 +547,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 +588,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

@@ -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,59 @@ 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 controllers.
*/
val controllers: 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 renderers.
*/
val renderers: Seq[(String, Renderer)] = 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.foreach { case (id, in) =>
registry.addImage(id, in)
}
controllers.foreach { case (path, controller) =>
registry.addController(path, controller)
}
javaScripts.foreach { case (path, script) =>
registry.addJavaScript(path, script)
}
renderers.foreach { case (extension, renderer) =>
registry.addRenderer(extension, renderer)
}
}
/**
* 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,10 @@ 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
)
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
@@ -30,34 +35,52 @@ 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))
}
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = {
addController(path, controller)
}
def getControllers(): List[(ControllerBase, String)] = controllers.toList
def addJavaScript(path: String, script: String): Unit = {
javaScripts += Tuple2(path, script)
javaScripts += ((path, script))
}
//def getJavaScripts(): List[(String, String)] = javaScripts.toList
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
private case class GlobalAction(
method: String,
path: String,
@@ -89,7 +112,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 +142,7 @@ object PluginRegistry {
}
// Initialize
plugin.initialize(instance)
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
@@ -137,10 +160,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,44 @@
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)(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,
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

@@ -0,0 +1,169 @@
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, 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

@@ -40,15 +40,19 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case auth => decodeAuthHeader(auth).split(":", 2) 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)
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)
}
} else {
requireAuth(response)
chain.doFilter(req, wrappedResponse)
}
}
case _ => requireAuth(response)

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

@@ -63,10 +63,10 @@ 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

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 {
@@ -713,7 +794,12 @@ object JGitUtil {
def getBranches(owner: String, name: String, defaultBranch: String): 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{
@@ -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))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)
@@ -92,14 +91,13 @@ class Mailer(private val smtp: Smtp) extends Notifier {
.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

@@ -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

@@ -123,8 +123,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 "")
@@ -175,6 +177,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, Renderer}
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.
*/
@@ -107,23 +97,22 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: 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, 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("$", "\\$"))
}
}
@@ -203,7 +192,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>

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

@@ -90,13 +90,32 @@
</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

@@ -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

@@ -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){

View File

@@ -8,21 +8,17 @@
<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/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,6 +29,12 @@
<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">
@@ -63,11 +65,11 @@
<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>
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user" aria-label="Account settings"></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/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench" aria-label="Administration"></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>
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt" aria-label="Sign out"></i></a>
} else {
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
}

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)
</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

@@ -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,

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

@@ -701,6 +701,12 @@ span.simplified-path {
padding-left: 16px;
}
.btn-pullrequest-branch{
background: none;
border: 1px solid #0088cc;
color: #0088cc;
}
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
@@ -1145,6 +1151,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 +1301,7 @@ ul.collaborator {
list-style-type: none;
margin-left: 0px;
}
ul.collaborator li {
background-color: #eee;
border: 1px solid #ccc;
@@ -1160,7 +1309,7 @@ ul.collaborator li {
padding: 6px;
margin-bottom: 2px;
}
ul.collaborator li:hover {
background-color: #f8f8f8;
}
@@ -1219,6 +1368,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 {
@@ -1399,3 +1550,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.

After

Width:  |  Height:  |  Size: 949 B

View File

@@ -12,6 +12,12 @@ $(function(){
$('a[data-toggle=tooltip]').tooltip();
$('li[data-toggle=tooltip]').tooltip();
// activate hotkey
$('a[data-hotkey]').each(function(){
var target = this;
$(document).bind('keydown', $(target).data('hotkey'), function(){ target.click(); });
});
// anchor icon for markdown
$('.markdown-head').mouseenter(function(e){
$(e.target).children('a.markdown-anchor-link').show();
@@ -103,7 +109,7 @@ function JsDiffRender(params){
var headTextLines = (params.newText==="")?[]:params.newText.split(/\r\n|\r|\n/);
var sm, ctx;
if(params.ignoreSpace){
var ignoreSpace = function(a){ return a.replace(/\s+/,' ').replace(/^\s+|\s+$/,''); };
var ignoreSpace = function(a){ return a.replace(/\s+/g,''); };
sm = new difflib.SequenceMatcher(
$.map(baseTextLines, ignoreSpace),
$.map(headTextLines, ignoreSpace));
@@ -334,3 +340,366 @@ $.extend(JsDiffRender.prototype,{
return ret;
}
});
/**
* scroll target into view ( on bottom edge, or on top edge)
*/
function scrollIntoView(target){
target = $(target);
var $window = $(window);
var docViewTop = $window.scrollTop();
var docViewBottom = docViewTop + $window.height();
var elemTop = target.offset().top;
var elemBottom = elemTop + target.height();
if(elemBottom > docViewBottom){
$('html, body').scrollTop(elemBottom - $window.height());
}else if(elemTop < docViewTop){
$('html, body').scrollTop(elemTop);
}
}
/**
* escape html
*/
function escapeHtml(text){
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;').replace(/>/g,'&gt;');
}
/**
* calculate string ranking for path.
* Original ported from:
* http://joshaven.com/string_score
* https://github.com/joshaven/string_score
*
* Copyright (C) 2009-2011 Joshaven Potter <yourtech@@gmail.com>
* Special thanks to all of the contributors listed here https://github.com/joshaven/string_score
* MIT license: http://www.opensource.org/licenses/mit-license.php
*/
function string_score(string, word) {
'use strict';
var zero = {score:0,matchingPositions:[]};
// If the string is equal to the word, perfect match.
if (string === word || word === "") { return {score:1, matchingPositions:[]}; }
var lString = string.toUpperCase(),
strLength = string.length,
lWord = word.toUpperCase(),
wordLength = word.length;
return calc(zero, 0, 0, 0, 0, []);
function calc(score, startAt, skip, runningScore, i, matchingPositions){
if( i < wordLength) {
var charScore = 0;
// Find next first case-insensitive match of a character.
var idxOf = lString.indexOf(lWord[i], skip);
if (-1 === idxOf) { return score; }
score = calc(score, startAt, idxOf+1, runningScore, i, matchingPositions);
if (startAt === idxOf) {
// Consecutive letter & start-of-string Bonus
charScore = 0.8;
} else {
charScore = 0.1;
// Acronym Bonus
// Weighing Logic: Typing the first character of an acronym is as if you
// preceded it with two perfect character matches.
if (/^[^A-Za-z0-9]/.test(string[idxOf - 1])){
charScore += 0.7;
}else if(string[idxOf]==lWord[i]) {
// Upper case bonus
charScore += 0.2;
// Camel case bonus
if(/^[a-z]/.test(string[idxOf - 1])){
charScore += 0.5;
}
}
}
// Same case bonus.
if (string[idxOf] === word[i]) { charScore += 0.1; }
// next round
return calc(score, idxOf + 1, idxOf + 1, runningScore + charScore, i+1, matchingPositions.concat(idxOf));
}else{
// skip non match folder
var effectiveLength = strLength;
if(matchingPositions.length){
var lastSlash = string.lastIndexOf('/',matchingPositions[0]);
if(lastSlash!==-1){
effectiveLength = strLength-lastSlash;
}
}
// Reduce penalty for longer strings.
var finalScore = 0.5 * (runningScore / effectiveLength + runningScore / wordLength);
if ((lWord[0] === lString[0]) && (finalScore < 0.85)) {
finalScore += 0.15;
}
if(score.score >= finalScore){
return score;
}
return {score:finalScore, matchingPositions:matchingPositions};
}
}
}
/**
* sort by string_score.
* @param word {String} search word
* @param strings {Array[String]} search targets
* @param limit {Integer} result limit
* @return {Array[{score:"float matching score", string:"string target string", matchingPositions:"Array[Interger] matchng positions"}]}
*/
function string_score_sort(word, strings, limit){
var ret = [], i=0, l = (word==="")?Math.min(strings.length, limit):strings.length;
for(; i < l; i++){
var score = string_score(strings[i],word);
if(score.score){
score.string = strings[i];
ret.push(score);
}
}
ret.sort(function(a,b){
var s = b.score - a.score;
if(s === 0){
return a.string > b.string ? 1 : -1;
}
return s;
});
ret = ret.slice(0,limit);
return ret;
}
/**
* highlight by result.
* @param score {string:"string target string", matchingPositions:"Array[Interger] matchng positions"}
* @param highlight tag ex: '<b>'
* @return array of highlighted html elements.
*/
function string_score_highlight(result, tag){
var str = result.string, msp=0;
return hilight([], 0, result.matchingPositions[msp]);
function hilight(html, c, mpos){
if(mpos === undefined){
return html.concat(document.createTextNode(str.substr(c)));
}else{
return hilight(html.concat([
document.createTextNode(str.substring(c,mpos)),
$(tag).text(str[mpos])]),
mpos+1, result.matchingPositions[++msp]);
}
}
}
/****************************************************************************/
/* Diff */
/****************************************************************************/
// add naturalWidth and naturalHeight for ie 8
function setNatural(img) {
if(typeof img.naturalWidth == 'undefined'){
var tmp = new Image();
tmp.src = img.src;
img.naturalWidth = tmp.width;
img.naturalHeight = tmp.height;
}
}
/**
* onload handler
* @param img <img>
*/
function onLoadedDiffImages(img){
setNatural(img);
img = $(img);
img.show();
var tb = img.parents(".diff-image-render");
// Find images. If the image has not loaded yet, value is undefined.
var old = tb.find(".diff-old img.diff-image:visible")[0];
var neo = tb.find(".diff-new img.diff-image:visible")[0];
imageDiff.appendImageMeta(tb, old, neo);
if(old && neo){
imageDiff.createToolSelector(old, neo).appendTo(tb.parent());
}
}
var imageDiff ={
/** append image meta div after image nodes.
* @param tb <div class="diff-image-2up">
* @param old <img>||undefined
* @param neo <img>||undefined
*/
appendImageMeta:function(tb, old, neo){
old = old || {};
neo = neo || {};
tb.find(".diff-meta").remove();
// before loaded, image is not visible.
tb.find("img.diff-image:visible").each(function(){
var div = $('<p class="diff-meta"><b>W:</b><span class="w"></span> | <b>W:</b><span class="h"></span></p>');
div.find('.w').text(this.naturalWidth+"px").toggleClass("diff", old.naturalWidth != neo.naturalWidth);
div.find('.h').text(this.naturalHeight+"px").toggleClass("diff", old.naturalHeight != neo.naturalHeight);
div.appendTo(this.parentNode);
});
},
/** check this browser can use canvas tag.
*/
hasCanvasSupport:function(){
if(!this.hasCanvasSupport.hasOwnProperty('resultCache')){
this.hasCanvasSupport.resultCache = (typeof $('<canvas>')[0].getContext)=='function';
}
return this.hasCanvasSupport.resultCache;
},
/** create toolbar
* @param old <img>
* @param neo <img>
* @return jQuery(<ul class="image-diff-tools">)
*/
createToolSelector:function(old, neo){
var self = this;
return $('<ul class="image-diff-tools">'+
'<li data-mode="diff2up" class="active">2-up</li>'+
'<li data-mode="swipe">Swipe</li>'+
'<li data-mode="onion">Onion Skin</li>'+
'<li data-mode="difference" class="need-canvas">Difference</li>'+
'<li data-mode="blink">Blink</li>'+
'</ul>')
.toggleClass('no-canvas', !this.hasCanvasSupport())
.on('click', 'li', function(e){
var td = $(this).parents("td");
$(e.delegateTarget).find('li').each(function(){ $(this).toggleClass('active',this == e.target); });
var mode = $(e.target).data('mode');
td.find(".diff-image-render").hide();
// create div if not created yet
if(td.find(".diff-image-render."+mode).show().length===0){
self[mode](old, neo).insertBefore(e.delegateTarget).addClass("diff-image-render");
}
return false;
});
},
/** (private) calc size from images and css (const)
* @param old <img>
* @param neo <img>
*/
calcSizes:function(old, neo){
var maxWidth = 869 - 20 - 20 - 4; // set by css
var h = Math.min(Math.max(old.naturalHeight, neo.naturalHeight),maxWidth);
var w = Math.min(Math.max(old.naturalWidth, neo.naturalWidth),maxWidth);
var oldRate = Math.min(h/old.naturalHeight, w/old.naturalWidth);
var neoRate = Math.min(h/neo.naturalHeight, w/neo.naturalWidth);
var neoW = neo.naturalWidth*neoRate;
var neoH = neo.naturalHeight*neoRate;
var oldW = old.naturalWidth*oldRate;
var oldH = old.naturalHeight*oldRate;
var paddingLeft = (maxWidth/2)-Math.max(neoW,oldW)/2;
return {
height:Math.max(oldH, neoH),
width:w,
padding:paddingLeft,
paddingTop:20, // set by css
barWidth:200, // set by css
old:{ rate:oldRate, width:oldW, height:oldH },
neo:{ rate:neoRate, width:neoW, height:neoH }
};
},
/** (private) create div
* @param old <img>
* @param neo <img>
*/
stack:function(old, neo){
var size = this.calcSizes(old, neo);
var diffNew = $('<div class="diff-new">')
.append($("<img>").attr('src',neo.src).css({width:size.neo.width, height:size.neo.height}));
var diffOld = $('<div class="diff-old">')
.append($("<img>").attr('src',old.src).css({width:size.old.width, height:size.old.height}));
var handle = $('<span class="diff-swipe-handle icon icon-resize-horizontal"></span>')
.css({marginTop:size.height-5});
var bar = $('<hr class="diff-silde-bar">').css({top:size.height+size.paddingTop});
var div = $('<div class="diff-image-stack">')
.css({height:size.height+size.paddingTop, paddingLeft:size.padding})
.append(diffOld, diffNew, bar, handle);
return {
neo:diffNew,
old:diffOld,
size:size,
handle:handle,
bar:bar,
div:div,
/* add event listener 'on mousemove' */
onMoveHandleOnBar:function(callback){
div.on('mousemove',function(e){
var x = Math.max(Math.min((e.pageX - bar.offset().left), size.barWidth), 0);
handle.css({left:x});
callback(x, e);
});
}
};
},
/** create swipe box
* @param old <img>
* @param neo <img>
* @return jQuery(<div class="diff-image-stack swipe">)
*/
swipe:function(old, neo){
var stack = this.stack(old, neo);
function setX(x){
stack.neo.css({width:x});
stack.handle.css({left:x+stack.size.padding});
}
setX(stack.size.neo.width/2);
stack.div.on('mousemove',function(e){
setX(Math.max(Math.min(e.pageX - stack.neo.offset().left, stack.size.neo.width),0));
});
return stack.div.addClass('swipe');
},
/** create blink box
* @param old <img>
* @param neo <img>
* @return jQuery(<div class="diff-image-stack blink">)
*/
blink:function(old, neo){
var stack = this.stack(old, neo);
stack.onMoveHandleOnBar(function(x){
stack.neo.toggle(Math.floor(x) % 2 === 0);
});
return stack.div.addClass('blink');
},
/** create onion skin box
* @param old <img>
* @param neo <img>
* @return jQuery(<div class="diff-image-stack onion">)
*/
onion:function(old, neo){
var stack = this.stack(old, neo);
stack.neo.css({opacity:0.5});
stack.onMoveHandleOnBar(function(x){
stack.neo.css({opacity:x/stack.size.barWidth});
});
return stack.div.addClass('onion');
},
/** create difference box
* @param old <img>
* @param neo <img>
* @return jQuery(<div class="diff-image-stack difference">)
*/
difference:function(old, neo){
var size = this.calcSizes(old,neo);
var canvas = $('<canvas>').attr({width:size.width, height:size.height})[0];
var context = canvas.getContext('2d');
context.clearRect(0, 0, size.width, size.height);
context.drawImage(neo, 0, 0, size.neo.width, size.neo.height);
var neoData = context.getImageData(0, 0, size.neo.width, size.neo.height).data;
context.clearRect(0, 0, size.width, size.height);
context.drawImage(old, 0, 0, size.old.width, size.old.height);
var c = context.getImageData(0, 0, size.neo.width, size.neo.height);
var cData = c.data;
for (var i = cData.length -1; i>0; i --){
cData[i] = (i%4===3) ? Math.max(cData[i], neoData[i]) : Math.abs(cData[i] - neoData[i]);
}
context.putImageData(c, 0, 0);
return $('<div class="diff-image-stack difference">').append(canvas);
}
};

View File

@@ -0,0 +1,204 @@
/*jslint browser: true*/
/*jslint jquery: true*/
/*
* jQuery Hotkeys Plugin
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Based upon the plugin by Tzury Bar Yochay:
* https://github.com/tzuryby/jquery.hotkeys
*
* Original idea by:
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
*/
/*
* One small change is: now keys are passed by object { keys: '...' }
* Might be useful, when you want to pass some other data to your handler
*/
(function(jQuery) {
jQuery.hotkeys = {
version: "0.8",
specialKeys: {
8: "backspace",
9: "tab",
10: "return",
13: "return",
16: "shift",
17: "ctrl",
18: "alt",
19: "pause",
20: "capslock",
27: "esc",
32: "space",
33: "pageup",
34: "pagedown",
35: "end",
36: "home",
37: "left",
38: "up",
39: "right",
40: "down",
45: "insert",
46: "del",
59: ";",
61: "=",
96: "0",
97: "1",
98: "2",
99: "3",
100: "4",
101: "5",
102: "6",
103: "7",
104: "8",
105: "9",
106: "*",
107: "+",
109: "-",
110: ".",
111: "/",
112: "f1",
113: "f2",
114: "f3",
115: "f4",
116: "f5",
117: "f6",
118: "f7",
119: "f8",
120: "f9",
121: "f10",
122: "f11",
123: "f12",
144: "numlock",
145: "scroll",
173: "-",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'"
},
shiftNums: {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ": ",
"'": "\"",
",": "<",
".": ">",
"/": "?",
"\\": "|"
},
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
textAcceptingInputTypes: [
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
"datetime-local", "search", "color", "tel"],
// default input types not to bind to unless bound directly
textInputTypes: /textarea|input|select/i,
options: {
filterInputAcceptingElements: true,
filterTextInputs: true,
filterContentEditable: true
}
};
function keyHandler(handleObj) {
if (typeof handleObj.data === "string") {
handleObj.data = {
keys: handleObj.data
};
}
// Only care when a possible input has been specified
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
return;
}
var origHandler = handleObj.handler,
keys = handleObj.data.keys.toLowerCase().split(" ");
handleObj.handler = function(event) {
// Don't fire in text-accepting inputs that we didn't directly bind to
if (this !== event.target &&
(jQuery.hotkeys.options.filterInputAcceptingElements &&
jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
(jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
(jQuery.hotkeys.options.filterTextInputs &&
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
return;
}
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
character = String.fromCharCode(event.which).toLowerCase(),
modif = "",
possible = {};
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
if (event[specialKey + 'Key'] && special !== specialKey) {
modif += specialKey + '+';
}
});
// metaKey is triggered off ctrlKey erronously
if (event.metaKey && !event.ctrlKey && special !== "meta") {
modif += "meta+";
}
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
modif = modif.replace("alt+ctrl+shift+", "hyper+");
}
if (special) {
possible[modif + special] = true;
}
else {
possible[modif + character] = true;
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
if (modif === "shift+") {
possible[jQuery.hotkeys.shiftNums[character]] = true;
}
}
for (var i = 0, l = keys.length; i < l; i++) {
if (possible[keys[i]]) {
return origHandler.apply(this, arguments);
}
}
};
}
jQuery.each(["keydown", "keyup", "keypress"], function() {
jQuery.event.special[this] = {
add: keyHandler
};
});
})(jQuery || this.jQuery || window.jQuery);

View File

@@ -29,6 +29,9 @@ DAMAGE.
***/
/* Author: Chas Emerick <cemerick@snowtide.com> */
__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
function __hasOwnProperty(obj, target){
return Object.prototype.hasOwnProperty.call(obj, target);
}
difflib = {
defaultJunkFunction: function (c) {
@@ -93,7 +96,7 @@ difflib = {
// replacement for python's dict.get function -- need easy default values
__dictget: function (dict, key, defaultValue) {
return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
return __hasOwnProperty(dict, key) ? dict[key] : defaultValue;
},
SequenceMatcher: function (a, b, isjunk) {
@@ -122,7 +125,7 @@ difflib = {
var populardict = {};
for (var i = 0; i < b.length; i++) {
var elt = b[i];
if (b2j.hasOwnProperty(elt)) {
if (__hasOwnProperty(b2j, elt)) {
var indices = b2j[elt];
if (n >= 200 && indices.length * 100 > n) {
populardict[elt] = 1;
@@ -151,7 +154,7 @@ difflib = {
}
}
for (var elt in b2j) {
if (b2j.hasOwnProperty(elt) && isjunk(elt)) {
if (__hasOwnProperty(b2j, elt) && isjunk(elt)) {
junkdict[elt] = 1;
delete b2j[elt];
}

View File

@@ -66,7 +66,7 @@ table.diff thead th.texttitle {
}
table.diff tbody td {
padding:0px .4em;
padding-top:.4em;
padding-top:.2em;
vertical-align:top;
border-top: none;
}

View File

@@ -90,11 +90,12 @@ class JsonFormatSpec extends Specification {
user = apiUser,
body= "Me too",
created_at= date1,
updated_at= date1)
updated_at= date1)(RepositoryName("octocat","Hello-World"), 100)
val apiCommentJson = s"""{
"id": 1,
"body": "Me too",
"user": $apiUserJson,
"html_url" : "${context.baseUrl}/octocat/Hello-World/issues/100#comment-1",
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z"
}"""
@@ -157,13 +158,15 @@ class JsonFormatSpec extends Specification {
state = "open",
body = "I'm having a problem with this.",
created_at = date1,
updated_at = date1)
updated_at = date1)(RepositoryName("octocat","Hello-World"))
val apiIssueJson = s"""{
"number": 1347,
"state": "open",
"title": "Found a bug",
"body": "I'm having a problem with this.",
"user": $apiUserJson,
"comments_url": "${context.baseUrl}/api/v3/repos/octocat/Hello-World/issues/1347/comments",
"html_url": "${context.baseUrl}/octocat/Hello-World/issues/1347",
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z"
}"""

View File

@@ -5,6 +5,7 @@ import gitbucket.core.util.JGitUtil
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.GitSpecUtil._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
@@ -14,51 +15,22 @@ import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.treewalk._
import org.specs2.mutable.Specification
import java.io.File
import java.nio.file._
import java.util.Date
class MergeServiceSpec extends Specification {
sequential
val service = new MergeService{}
val branch = "master"
val issueId = 10
def initRepository(owner:String, name:String) = {
val repo1Dir = getRepositoryDir(owner, name)
RepositoryCache.clear()
FileUtils.deleteQuietly(repo1Dir)
Files.createDirectories(repo1Dir.toPath())
JGitUtil.initRepository(repo1Dir)
using(Git.open(repo1Dir)){ git =>
createFile(git, s"refs/heads/master", "test.txt", "hoge" )
git.branchCreate().setStartPoint(s"refs/heads/master").setName(s"refs/pull/${issueId}/head").call()
}
repo1Dir
}
def createFile(git:Git, branch:String, name:String, content:String){
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(branch + "^{commit}")
builder.add(JGitUtil.createDirCacheEntry(name, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
branch, "dummy", "dummy@example.com", "Initial commit")
}
def getFile(git:Git, branch:String, path:String) = {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
val objectId = using(new TreeWalk(git.getRepository)){ walk =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
@scala.annotation.tailrec
def _getPathObjectId: ObjectId = walk.next match {
case true if(walk.getPathString == path) => walk.getObjectId(0)
case true => _getPathObjectId
case false => throw new Exception(s"not found ${branch} / ${path}")
}
_getPathObjectId
}
JGitUtil.getContentInfo(git, path, objectId)
def initRepository(owner:String, name:String): File = {
val dir = createTestRepository(getRepositoryDir(owner, name))
using(Git.open(dir)){ git =>
createFile(git, s"refs/heads/master", "test.txt", "hoge" )
git.branchCreate().setStartPoint(s"refs/heads/master").setName(s"refs/pull/${issueId}/head").call()
}
dir
}
def createConfrict(git:Git) = {
createFile(git, s"refs/heads/${branch}", "test.txt", "hoge2" )

View File

@@ -0,0 +1,33 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import org.specs2.mutable.Specification
class PullRequestServiceSpec extends Specification with ServiceSpecBase with PullRequestService with IssuesService {
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)
"PullRequestService.getPullRequestFromBranch" should {
"""
|return pull request if exists pull request from `branch` to `defaultBranch` and not closed.
|return pull request if exists pull request from `branch` to othre branch and not closed.
|return None if all pull request is closed""".stripMargin.trim in { withTestDB { implicit se =>
generateNewUserWithDBRepository("user1", "repo1")
generateNewUserWithDBRepository("user1", "repo2")
generateNewUserWithDBRepository("user2", "repo1")
generateNewPullRequest("user1/repo1/master", "user1/repo1/head2") // not target branch
generateNewPullRequest("user1/repo1/head1", "user1/repo1/master") // not target branch ( swap from, to )
generateNewPullRequest("user1/repo1/master", "user2/repo1/head1") // othre user
generateNewPullRequest("user1/repo1/master", "user1/repo2/head1") // othre repository
val r1 = swap(generateNewPullRequest("user1/repo1/master2", "user1/repo1/head1"))
val r2 = swap(generateNewPullRequest("user1/repo1/master", "user1/repo1/head1"))
val r3 = swap(generateNewPullRequest("user1/repo1/master4", "user1/repo1/head1"))
getPullRequestFromBranch("user1", "repo1", "head1", "master") must_== Some(r2)
updateClosed("user1", "repo1", r2._1.issueId, true)
getPullRequestFromBranch("user1", "repo1", "head1", "master").get must beOneOf(r1, r2)
updateClosed("user1", "repo1", r1._1.issueId, true)
updateClosed("user1", "repo1", r3._1.issueId, true)
getPullRequestFromBranch("user1", "repo1", "head1", "master") must beNone
} }
}
}

View File

@@ -32,23 +32,25 @@ trait ServiceSpecBase {
def generateNewAccount(name:String)(implicit s:Session):Account = {
AccountService.createAccount(name, name, name, s"${name}@example.com", false, None)
AccountService.getAccountByUserName(name).get
user(name)
}
def user(name:String)(implicit s:Session):Account = AccountService.getAccountByUserName(name).get
lazy val dummyService = new RepositoryService with AccountService with IssuesService with PullRequestService
with CommitStatusService (){}
def generateNewUserWithDBRepository(userName:String, repositoryName:String)(implicit s:Session):Account = {
val ac = generateNewAccount(userName)
val ac = AccountService.getAccountByUserName(userName).getOrElse(generateNewAccount(userName))
dummyService.createRepository(repositoryName, userName, None, false)
ac
}
def generateNewIssue(userName:String, repositoryName:String, requestUserName:String="root")(implicit s:Session): Int = {
def generateNewIssue(userName:String, repositoryName:String, loginUser:String="root")(implicit s:Session): Int = {
dummyService.createIssue(
owner = userName,
repository = repositoryName,
loginUser = requestUserName,
loginUser = loginUser,
title = "issue title",
content = None,
assignedUserName = None,
@@ -56,10 +58,10 @@ trait ServiceSpecBase {
isPullRequest = true)
}
def generateNewPullRequest(base:String, request:String)(implicit s:Session):(Issue, PullRequest) = {
def generateNewPullRequest(base:String, request:String, loginUser:String=null)(implicit s:Session):(Issue, PullRequest) = {
val Array(baseUserName, baseRepositoryName, baesBranch)=base.split("/")
val Array(requestUserName, requestRepositoryName, requestBranch)=request.split("/")
val issueId = generateNewIssue(baseUserName, baseRepositoryName, requestUserName)
val issueId = generateNewIssue(baseUserName, baseRepositoryName, Option(loginUser).getOrElse(requestUserName))
dummyService.createPullRequest(
originUserName = baseUserName,
originRepositoryName = baseRepositoryName,

View File

@@ -11,9 +11,10 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
val user1 = generateNewUserWithDBRepository("user1","repo1")
val user2 = generateNewUserWithDBRepository("user2","repo2")
val user3 = generateNewUserWithDBRepository("user3","repo3")
val (issue1, pullreq1) = generateNewPullRequest("user1/repo1/master1", "user2/repo2/master2")
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2")
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2")
val issueUser = user("root")
val (issue1, pullreq1) = generateNewPullRequest("user1/repo1/master1", "user2/repo2/master2", loginUser="root")
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
service.addWebHookURL("user1", "repo1", "webhook1-1")
service.addWebHookURL("user1", "repo1", "webhook1-2")
@@ -25,18 +26,19 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty
var r = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
r.size must_== 3
r((issue1, pullreq1, user1, user2)) must_== Set("webhook1-1","webhook1-2")
r((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r((issue1, issueUser, pullreq1, user1, user2)) must_== Set("webhook1-1","webhook1-2")
r((issue3, issueUser, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
// when closed, it not founds.
service.updateClosed("user1","repo1",issue1.issueId, true)
var r2 = service.getPullRequestsByRequestForWebhook("user2","repo2","master2").mapValues(_.map(_.url).toSet)
r2.size must_== 2
r2((issue3, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r2((issue32, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r2((issue3, issueUser, pullreq3, user3, user2)) must_== Set("webhook3-1","webhook3-2")
r2((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
} }
}
}

View File

@@ -0,0 +1,110 @@
package gitbucket.core.util
import gitbucket.core.model._
import gitbucket.core.util.Directory._
import gitbucket.core.util.ControlUtil._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.merge._
import org.eclipse.jgit.errors._
import java.nio.file._
import java.util.Date
import java.io.File
object GitSpecUtil {
def withTestFolder[U](f: File => U) {
val folder = new File(System.getProperty("java.io.tmpdir"), "test-" + System.nanoTime)
if(!folder.mkdirs()){
throw new java.io.IOException("can't create folder "+folder.getAbsolutePath)
}
try {
f(folder)
} finally {
FileUtils.deleteQuietly(folder)
}
}
def withTestRepository[U](f: Git => U) = withTestFolder(folder => using(Git.open(createTestRepository(folder)))(f))
def createTestRepository(dir: File): File = {
RepositoryCache.clear()
FileUtils.deleteQuietly(dir)
Files.createDirectories(dir.toPath())
JGitUtil.initRepository(dir)
dir
}
def createFile(git: Git, branch: String, name: String, content: String,
autorName: String = "dummy", autorEmail: String = "dummy@example.com",
message: String = "test commit") {
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(branch + "^{commit}")
if(headId!=null){
JGitUtil.processTree(git, headId){ (path, tree) =>
if(name != path){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
}
builder.add(JGitUtil.createDirCacheEntry(name, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
branch, autorName, autorEmail, message)
inserter.flush()
inserter.release()
}
def getFile(git: Git, branch: String, path: String) = {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
val objectId = using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
@scala.annotation.tailrec
def _getPathObjectId: ObjectId = walk.next match {
case true if (walk.getPathString == path) => walk.getObjectId(0)
case true => _getPathObjectId
case false => throw new Exception(s"not found ${branch} / ${path}")
}
_getPathObjectId
}
JGitUtil.getContentInfo(git, path, objectId)
}
def mergeAndCommit(git: Git, into:String, branch:String, message:String = null):Unit = {
val repository = git.getRepository
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val mergeBaseTip = repository.resolve(into)
val mergeTip = repository.resolve(branch)
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
if(conflicted){
throw new RuntimeException("conflict!")
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent;
// creates merge commit
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(merger.getResultTreeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
// update refs
val refUpdate = repository.updateRef(into)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(true)
refUpdate.setRefLogIdent(committer)
refUpdate.update()
}
}

View File

@@ -0,0 +1,103 @@
package gitbucket.core.util
import org.specs2.mutable._
import GitSpecUtil._
class JGitUtilSpec extends Specification {
"getFileList(git: Git, revision: String, path)" should {
withTestRepository { git =>
def list(branch: String, path: String) =
JGitUtil.getFileList(git, branch, path).map(finfo => (finfo.name, finfo.message, finfo.isDirectory))
list("master", ".") mustEqual Nil
list("master", "dir/subdir") mustEqual Nil
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "README.md", "body1", message = "commit1")
list("master", ".") mustEqual List(("README.md", "commit1", false))
list("master", "dir/subdir") mustEqual Nil
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "README.md", "body2", message = "commit2")
list("master", ".") mustEqual List(("README.md", "commit2", false))
list("master", "dir/subdir") mustEqual Nil
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "dir/subdir/File3.md", "body3", message = "commit3")
list("master", ".") mustEqual List(("dir/subdir", "commit3", true), ("README.md", "commit2", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false))
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "dir/subdir/File4.md", "body4", message = "commit4")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit2", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "README5.md", "body5", message = "commit5")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit2", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
createFile(git, "master", "README.md", "body6", message = "commit6")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual Nil
list("branch", "dir/subdir") mustEqual Nil
git.branchCreate().setName("branch").setStartPoint("master").call()
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
createFile(git, "branch", "dir/subdir/File3.md", "body7", message = "commit7")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual List(("dir/subdir", "commit7", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
createFile(git, "master", "dir8/File8.md", "body8", message = "commit8")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual List(("dir/subdir", "commit7", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
createFile(git, "branch", "dir/subdir9/File9.md", "body9", message = "commit9")
list("master", ".") mustEqual List(("dir/subdir", "commit4", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit3", false), ("File4.md", "commit4", false))
list("branch", ".") mustEqual List(("dir", "commit9", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("branch", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
mergeAndCommit(git, "master", "branch", message = "merge10")
list("master", ".") mustEqual List(("dir", "commit9", true), ("dir8", "commit8", true), ("README.md", "commit6", false), ("README5.md", "commit5", false))
list("master", "dir/subdir") mustEqual List(("File3.md", "commit7", false), ("File4.md", "commit4", false))
}
}
"getFileList subfolder multi-origin (issue #721)" should {
withTestRepository { git =>
def list(branch: String, path: String) =
JGitUtil.getFileList(git, branch, path).map(finfo => (finfo.name, finfo.message, finfo.isDirectory))
createFile(git, "master", "README.md", "body1", message = "commit1")
createFile(git, "branch", "test/text2.txt", "body2", message = "commit2")
mergeAndCommit(git, "master", "branch", message = "merge3")
list("master", "test") mustEqual List(("text2.txt", "commit2", false))
}
}
}

View File

@@ -100,6 +100,7 @@ class AvatarImageProviderSpec extends Specification with Mockito {
isCreateRepoOptionPublic = true,
gravatar = useGravatar,
notification = false,
activityLogLimit = None,
ssh = false,
sshPort = None,
smtp = None,