Compare commits

..

125 Commits
1.6 ... 1.7

Author SHA1 Message Date
Naoki Takezoe
6e169ab3c2 Update README.md 2013-10-26 01:46:39 +09:00
Naoki Takezoe
6ac27e89b3 Update README.md 2013-10-26 01:45:59 +09:00
takezoe
2235dab550 (refs #174)Fix commit hook for DELETE command. 2013-10-26 01:40:26 +09:00
Naoki Takezoe
7604c2172f Update README.md 2013-10-25 04:44:03 +09:00
takezoe
1e750f4b9d (refs #171)Fix link target of pull request number. 2013-10-25 04:28:58 +09:00
takezoe
d1f0d01ae8 Small fix for #170 2013-10-25 04:05:34 +09:00
Naoki Takezoe
167a0f28b2 Merge pull request #170 from jtyr/master
Allow to force HTTPS scheme
2013-10-24 12:00:25 -07:00
takezoe
06be5266fd Fix refs message. 2013-10-24 15:04:32 +09:00
takezoe
60e7165983 (refs #104)Zip file is exported from the bare repository directly. 2013-10-24 11:29:23 +09:00
takezoe
6dbfc12896 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-24 11:07:26 +09:00
takezoe
6d4b3e54d0 Fix style problem in Wiki. 2013-10-24 11:03:47 +09:00
takezoe
2968b92677 Add the commit link to refs comment. 2013-10-24 04:25:50 +09:00
takezoe
0d0bf4ad3f Don't add group account as a collaborator. 2013-10-24 02:08:14 +09:00
takezoe
53fa60b0f8 Exclude owner from assigned user list in the group repository. 2013-10-24 02:00:14 +09:00
Jiri Tyr
99517fa508 Fixing command line options in init.d script
Adding missing "--host" option and fixing the declaration of other
command line options.
2013-10-21 22:47:27 +01:00
Jiri Tyr
2e239d16d4 Refactorization of the https command line option
Renaming the flag variable and the Connector class.
2013-10-21 22:45:34 +01:00
Jiri Tyr
6de5babd5b Merge remote-tracking branch 'upstream/master' 2013-10-21 17:40:42 +01:00
takezoe
f3ad1a019d Small fix for #147 2013-10-22 01:09:22 +09:00
Naoki Takezoe
90ab882e8e Merge pull request #147 from xuwei-k/AccountServiceSpec
add AccountServiceSpec
2013-10-21 09:01:11 -07:00
Jiri Tyr
53269096a6 Allow to force HTTPS scheme
If the standalone GitBucket instance runs behind HTTPS proxy, the
repository URL always shows HTTP scheme dispute the fact that the
connection is HTTPS. This patch is adding a command line option which
allows to force the HTTPS scheme.
2013-10-21 14:32:53 +01:00
shimamoto
254509f243 Fix bug. 2013-10-21 22:32:46 +09:00
Naoki Takezoe
a697f186af Merge pull request #164 from smly/feature/bugfix-redirect-encode
Fix a problem to redirect wikipage named by multi-byte characters
2013-10-20 19:33:37 -07:00
takezoe
2316a80be9 (refs #163)Remove escaping for '.' 2013-10-21 10:53:36 +09:00
shimamoto
bbcb04b263 Fix bug. 2013-10-21 05:16:55 +09:00
shimamoto
7afe7fbb5f (refs #103) Add issue comment deletion. 2013-10-21 05:11:53 +09:00
takezoe
7c7da7379d (refs #163)Fixed 2013-10-19 18:48:50 +09:00
Kohei Ozaki
37358e9c8c Fix a problem to redirect wikipage named by multi-byte characters
In some specific case, redirect path (created from route params) is incorrect.
`redirectUrl` is expected to be encoded,
but scalatra decodes route params by rl.UrlCodingUtils via ScalatraBase.UriDecoder.
To avoid this problem, I add dirty workaround to encode redirect path.
2013-10-19 14:20:35 +09:00
Naoki Takezoe
41941df87a Update README.md 2013-10-19 13:58:04 +09:00
takezoe
bf2ed81eb1 (refs #163)Allow '.' in user name. 2013-10-19 12:56:55 +09:00
Naoki Takezoe
2d85d41e9c Merge pull request #161 from jtyr/master
System support for RedHat
2013-10-17 10:36:09 -07:00
Naoki Takezoe
e5e7b2484c Describe version of Servlet container 2013-10-18 00:59:16 +09:00
Jiri Tyr
6058552654 System support for RedHat
This commit is adding init.d script and sysconfig file which allows to
run GitBucket in the standalone mode. It also adds the spec file which
allows to build RPM package.
2013-10-17 01:39:47 +01:00
takezoe
f40c7ff4fa Fix testcase. 2013-10-16 04:47:43 +09:00
takezoe
da62c6181e (refs #33)Fix avatar icon and account link in the commits page for pull request. 2013-10-16 04:10:00 +09:00
takezoe
4d066738eb Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/view/AvatarImageProvider.scala
2013-10-16 02:34:12 +09:00
Naoki Takezoe
cb12d03262 Merge pull request #143 from chris-vanvranken/master
retrieve LDAP emails as lowercase to avoid confusion
2013-10-15 10:28:05 -07:00
takezoe
9a6a2d9b78 (refs #33)Improve avatar image searching behavior. 2013-10-16 02:24:40 +09:00
takezoe
ff0af477cb Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/view/helpers.scala
2013-10-16 01:51:44 +09:00
Chris Van Vranken
05adf9345f retrieve LDAP emails as lowercase to avoid confusing gravatar 2013-10-14 13:00:29 -04:00
Naoki Takezoe
ba70fdda48 Merge pull request #156 from xuwei-k/deprecated
fix deprecation warning
2013-10-14 06:42:35 -07:00
Naoki Takezoe
3885fcb2ec Merge pull request #157 from ssogabe/failonerror
Abort build process if sbt reports errors
2013-10-14 06:42:18 -07:00
ssogabe
99800a27f5 Abort build process if sbt reports errors 2013-10-14 15:01:30 +09:00
takezoe
107622942b (refs #127)Fix testcase. 2013-10-14 14:54:38 +09:00
xuwei-k
9794f14a65 fix deprecation warning. use HttpClientBuilder
https://github.com/apache/httpclient/blob/4.3/httpclient/src/main/java-deprecated/org/apache/http/impl/client/DefaultHttpClient.java#L113
2013-10-14 14:52:42 +09:00
xuwei-k
af759a815f add scalacOptions 2013-10-14 14:49:52 +09:00
takezoe
0e7078c479 (refs #151)Add unique checking to group creation. 2013-10-14 14:44:26 +09:00
Naoki Takezoe
83107c7974 Merge pull request #155 from HairyFotr/patch-2
Add plugin, Update versions
2013-10-13 22:13:16 -07:00
takezoe
ff9b2dbe93 (refs #127)Display full name at the account page. 2013-10-14 14:05:25 +09:00
takezoe
ebf4e5f2e9 Merge branch 'account-full-name' of https://github.com/robinst/gitbucket into robinst-account-full-name
Conflicts:
	src/main/scala/app/PullRequestsController.scala
2013-10-14 13:40:06 +09:00
Naoki Takezoe
21c30583e5 Merge pull request #153 from xuwei-k/number-format-exception
avoid NumberFormatException
2013-10-13 21:32:52 -07:00
takezoe
d6c9ace306 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-14 13:25:01 +09:00
takezoe
faf1252597 (refs #154)Remove comparison for original repository which does not exist. 2013-10-14 13:24:49 +09:00
HairyFotr
7b2ee25ea2 Add plugin, Update versions 2013-10-13 21:25:44 +02:00
xuwei-k
5a3207ae42 avoid NumberFormatException 2013-10-14 03:26:47 +09:00
Naoki Takezoe
3eab4955b9 Merge pull request #152 from xuwei-k/avoid-option-get
refactoring. avoid Option#get
2013-10-13 09:41:09 -07:00
xuwei-k
d772fc3ba2 refactoring. avoid Option#get 2013-10-14 01:18:31 +09:00
Naoki Takezoe
7de0a3fd70 Fix link to IIS installation wiki page. 2013-10-13 10:47:06 +09:00
Naoki Takezoe
eb8710a336 Merge pull request #144 from chris-vanvranken/patch-1
Add note that installation on windows server IIS is possible
2013-10-12 18:45:30 -07:00
Naoki Takezoe
25c55ecbd0 Merge pull request #150 from ssogabe/specs2
configure sbt to use the junitxml option with specs2
2013-10-12 18:31:13 -07:00
ssogabe
280df2cedd configure sbt to use the junitxml option with specs2 2013-10-13 09:38:52 +09:00
Naoki Takezoe
5ba9c86bee Merge pull request #149 from jparound30/activity_problem
Fix activity message problem (related to #120)
2013-10-12 08:14:13 -07:00
jparound30
faa6591d27 Fix activity message problem (related to #120) 2013-10-12 20:50:35 +09:00
takezoe
841d442f0d (refs #131)Don't create Dropzone if image has been registered. 2013-10-12 02:54:33 +09:00
xuwei-k
3351eabc4f add AccountServiceSpec 2013-10-11 13:08:58 +09:00
takezoe
006e1bc61e Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-11 03:10:10 +09:00
takezoe
35d1b4ea37 (refs #142)Hide "Fork" button for repositories which have no commit. 2013-10-11 03:09:25 +09:00
Naoki Takezoe
b0c5069695 Merge pull request #145 from xuwei-k/refactoring
refactoring
2013-10-10 11:01:26 -07:00
xuwei-k
dae0d0ad4b use FileUtil.withTmpDir and FileUtil.using 2013-10-11 02:44:03 +09:00
xuwei-k
79e560b7bf add FileUtil.withTmpDir 2013-10-11 02:44:03 +09:00
takezoe
cf79ac1069 (refs #142)Fix NoSuchElementException for empty repository. 2013-10-11 02:42:07 +09:00
Chris Van Vranken
8aab7a16c4 Add note that installation on windows server IIS is possible 2013-10-10 12:41:10 -04:00
shimamoto
c16b89b0be (refs #99) Improved to configure the FROM field of the email. 2013-10-10 23:57:45 +09:00
takezoe
25bbc00ff3 Display repository search field on pull request pages. 2013-10-10 19:00:56 +09:00
Tomofumi Tanaka
e667b6c139 (refs #139) Add info log for debugging LDAP Auth 2013-10-10 00:57:46 +09:00
Naoki Takezoe
195364223f Merge pull request #140 from xuwei-k/remove-unnecessary-code
remove unnecessary code
2013-10-08 21:17:50 -07:00
xuwei-k
84ce2cac8d remove unnecessary code 2013-10-09 11:09:06 +09:00
takezoe
f3507cf465 (refs #117)Add build.properties to fix sbt version. 2013-10-09 10:44:52 +09:00
Tomofumi Tanaka
f74f2c47d3 (refs #120) URL encode tag name
URL encode tag name URL like branch name.
And rename encodeBranchName to encodeRefName.
2013-10-09 09:59:30 +09:00
takezoe
72b25591a5 Resolve length issue in Slick.
https://github.com/slick/slick/issues/170
2013-10-09 03:11:27 +09:00
Naoki Takezoe
fe23a5c6da Merge pull request #136 from talios/patch-1
Change link text to 'Older'
2013-10-07 18:21:30 -07:00
Mark Derricutt
49fbc5cb62 Change link text to 'Older'
Updates the link text for previous commits to read 'Older' and not 'Order'.
2013-10-08 11:34:02 +13:00
takezoe
5a19a307a9 Merge remote-tracking branch 'origin/master' 2013-10-08 03:08:06 +09:00
takezoe
c3ec52b391 (refs #128)Add javacOptions to specify 6 as compilation target. 2013-10-08 03:07:33 +09:00
Naoki Takezoe
f2d68be0a3 Merge pull request #134 from tanacasino/fix-link-use-urlencode
(refs #120) Use encodeBranchName to tab links
2013-10-07 09:16:52 -07:00
Tomofumi Tanaka
c1f98ac481 (refs #120) Use encodeBranchName to tab links 2013-10-07 23:55:27 +09:00
Naoki Takezoe
8287c84dc7 Merge pull request #126 from robinst/readme-padding
Add padding around repository readme content
2013-10-06 18:42:39 -07:00
Robin Stocker
13bff2963e Add full name to account and use it to create commits (#125)
The Git practice is to use the full name when creating commits, not a
user name. This commit fixes that by introducing a fullName field to
Account and using it when creating commits.

For migrating from earlier versions, the user name is used as an initial
value for the full name field.
2013-10-06 23:11:09 +02:00
Robin Stocker
035f3f9e02 Add padding around repository readme content
It looks quite bad without padding.
2013-10-06 22:12:03 +02:00
takezoe
65e6de5ba4 (refs #120)URL encode branch name except '/'. 2013-10-07 02:36:35 +09:00
takezoe
82ced9233a Remove debug code. 2013-10-06 23:23:04 +09:00
takezoe
e94411ebeb (refs #121)Create WebHookPayload only when web hook has been registered. 2013-10-06 23:22:29 +09:00
takezoe
b92b429ffa (refs #121)Configure maxIdleTime and soLingerTime. 2013-10-06 21:31:55 +09:00
takezoe
e457cfb212 Fix branch name. 2013-10-06 19:49:46 +09:00
takezoe
f1476c52e6 (refs #121)Optimize push performance for a lot of commit. 2013-10-06 18:31:09 +09:00
takezoe
332246aed6 Add testcase for AvatarImageProvider. 2013-10-06 17:40:10 +09:00
takezoe
1c5201dcf1 Merge branch 'buildXML' of https://github.com/ssogabe/gitbucket 2013-10-06 16:03:25 +09:00
takezoe
36880ace27 (refs #116)Add --host option to bind Jetty connector to the specified hostname. 2013-10-06 15:56:47 +09:00
Naoki Takezoe
0d55d6ef6b Merge pull request #122 from lucas-clemente/patch-1
Fix typo when assigning issues
2013-10-05 09:51:50 -07:00
Lucas Clemente
688bf645b4 Fix typo when assigning issues 2013-10-05 14:51:26 +02:00
takezoe
d5a14482a6 Fix for pull request #119 to take some part of design fix. 2013-10-05 12:58:26 +09:00
Jan-Henrik Bruhn
cc1e0030df Did a lot of design-optimizations
mainly added icons and set correct bootstrap classes for forms, but also used some new fonts, provided via google webfonts
2013-10-05 00:21:10 +02:00
takezoe
fcadcb34a2 Add testcase for Pagination. 2013-10-05 04:31:45 +09:00
takezoe
dd8f440be0 Add testcase. 2013-10-05 03:39:46 +09:00
takezoe
17bc422e7a (refs #84)Add jquery.elastic and apply to issue and comment textarea. 2013-10-04 09:32:32 +09:00
takezoe
380cdbcf75 Add FileUtil#getContentType(). 2013-10-04 04:17:30 +09:00
takezoe
f4f2bf34fc (refs #73)Add Wiki conflict detection and some fix. 2013-10-04 03:48:51 +09:00
Naoki Takezoe
ed713d80a9 Merge pull request #110 from tanacasino/fix/migration-skip
Fix bug dot not skip migration when first init
2013-10-03 09:41:13 -07:00
Tomofumi Tanaka
c39703c61c Fix bug dot not skip migration when first init 2013-10-03 21:58:44 +09:00
takezoe
537773f975 Add testcase example. 2013-10-03 14:02:54 +09:00
takezoe
f37eca7c61 (refs #109)Change link color for absent Wiki pages. 2013-10-03 13:49:09 +09:00
takezoe
40a52d5ad5 Clone Wiki working repository if it does not exist before reverting. 2013-10-03 13:48:31 +09:00
takezoe
d95bd20cbe Fix commit message for Wiki editing. 2013-10-03 11:44:15 +09:00
takezoe
70ca98d6a2 (refs #38)Add reverting wiki from history. 2013-10-03 03:42:38 +09:00
takezoe
cf7caf55da (refs #108)Add ZIP download button to the repository viewer tab. 2013-10-03 00:57:17 +09:00
takezoe
b74bff3b2e Add org.h2.Driver.load(). 2013-10-02 11:03:30 +09:00
takezoe
b2e4853976 Merge branch 'master' of https://github.com/takezoe/gitbucket 2013-10-02 03:31:28 +09:00
takezoe
aef3c5c121 (refs #106)Don't use DbStarter because GitBucket does not use tcp server and it also create connection for each transaction. 2013-10-02 03:31:01 +09:00
takezoe
4afbfcb016 (refs #106)Skip migration if the current version is illegal. 2013-10-02 02:48:49 +09:00
Naoki Takezoe
09f8cff4c9 Update README.md 2013-10-01 03:58:40 +09:00
Naoki Takezoe
9ecd162040 GitBucket 1.6 released. 2013-10-01 03:56:48 +09:00
Naoki Takezoe
8617f02b01 Update README.md 2013-10-01 03:56:19 +09:00
ssogabe
9c14ddda18 allow users to build a war on Linux 2013-09-27 22:13:23 +09:00
takezoe
c0713eaeda (refs #33)Use RequestCache instead of AccountService directly. 2013-07-18 15:55:59 +09:00
takezoe
000afa1ed6 Merge branch 'master' into #33_match-by-email
Conflicts:
	src/main/scala/util/JGitUtil.scala
	src/main/scala/view/helpers.scala
	src/main/twirl/repo/blob.scala.html
	src/main/twirl/repo/commit.scala.html
	src/main/twirl/repo/commits.scala.html
	src/main/twirl/repo/files.scala.html
2013-07-18 15:49:56 +09:00
takezoe
828688ddd0 (refs #33)Match committer by mail address. 2013-07-12 04:27:20 +09:00
86 changed files with 1735 additions and 531 deletions

View File

@@ -32,67 +32,85 @@ Installation
--------
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
2. Deploy it to the servlet container such as Tomcat or Jetty.
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
The default administrator account is **root** and password is **root**.
(Since 1.6) or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
- --port=[NUMBER]
- --prefix=[CONTEXTPATH]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
Release Notes
--------
### 1.6 - COMMING SOON!
- Web hook.
- Performance improvement for pull request.
- Executable war file.
- Specify suitable Content-Type for downloaded files in the repository viewer.
- Fixed some bugs.
### 1.7 - 26 Oct 2013
- Support working on Java6 in embedded Jetty mode
- Add ```--host``` option to bind specified host name in embedded Jetty mode
- Add ```--https=true``` option to use https in embedded Jetty mode
- Add full name as user property
- Change link color for absent Wiki pages
- Add ZIP download button to the repository viewer tab
- Improve ZIP exporting performance
- Expand issue and comment textarea for long text automatically
- Add conflict detection in Wiki
- Add reverting wiki page from history
- Match committer to user name by email address
- Mail notification sender is customizable
- Add link to changeset in refs comment for issues
- Fix some bugs
### 1.6 - 1 Oct 2013
- Web hook
- Performance improvement for pull request
- Executable war file
- Specify suitable Content-Type for downloaded files in the repository viewer
- Fix some bugs
### 1.5 - 4 Sep 2013
- Fork and pull request.
- LDAP authentication.
- Mail notification.
- Add an option to turn off the gravatar support.
- Add the branch tab in the repository viewer.
- Encoding auto detection for the file content in the repository viewer.
- Add favicon, header logo and icons for the timeline.
- Specify data directory via environment variable GITBUCKET_HOME.
- Fixed some bugs.
- Fork and pull request
- LDAP authentication
- Mail notification
- Add an option to turn off the gravatar support
- Add the branch tab in the repository viewer
- Encoding auto detection for the file content in the repository viewer
- Add favicon, header logo and icons for the timeline
- Specify data directory via environment variable GITBUCKET_HOME
- Fix some bugs
### 1.4 - 31 Jul 2013
- Group management.
- Repository search for code and issues.
- Display user related issues on the dashboard.
- Display participants avatar of issues on the issue page.
- Performance improvement for repository viewer.
- Alert by milestone due date.
- H2 database administration console.
- Fixed some bugs.
- Group management
- Repository search for code and issues
- Display user related issues on the dashboard
- Display participants avatar of issues on the issue page
- Performance improvement for repository viewer
- Alert by milestone due date
- H2 database administration console
- Fix some bugs
### 1.3 - 18 Jul 2013
- Batch updating for issues.
- Display assigned user on issue list.
- User icon and Gravatar support.
- Convert @xxxx to link to the account page.
- Add copy to clipboard button for git clone URL.
- Allows multi-byte characters as wiki page name.
- Allows to create the empty repository.
- Fixed some bugs.
- Batch updating for issues
- Display assigned user on issue list
- User icon and Gravatar support
- Convert @xxxx to link to the account page
- Add copy to clipboard button for git clone URL
- Allow multi-byte characters as wiki page name
- Allow to create the empty repository
- Fix some bugs
### 1.2 - 09 Jul 2013
- Added activity timeline.
- Bugfix for Git 1.8.1.5 or later.
- Allows multi-byte characters as label.
- Fixed some bugs.
- Add activity timeline
- Bugfix for Git 1.8.1.5 or later
- Allow multi-byte characters as label
- Fix some bugs
### 1.1 - 05 Jul 2013
- Fixed some bugs.
- Upgrade to JGit 3.0.
- Fix some bugs
- Upgrade to JGit 3.0
### 1.0 - 04 Jul 2013
- This is a first public release.
- This is a first public release

View File

@@ -9,13 +9,19 @@
<property name="jetty.version" value="8.1.8.v20121106"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
<os family="windows" />
</condition>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec command="sbt.bat clean package"/>
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
<arg line="clean compile test package" />
</exec>
</target>
<target name="embed" depends="war">
@@ -40,7 +46,7 @@
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${target.dir}/scala-${scala.version}/classes"
update = "true"
includes="JettyLauncher.class"/>
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
</target>
<target name="rename" depends="embed">
@@ -52,4 +58,4 @@
</target>
</project>
</project>

View File

@@ -0,0 +1,20 @@
# Bind host
#GITBUCKET_HOST=0.0.0.0
# Server port
#GITBUCKET_PORT=8080
# Force HTTPS scheme
#GITBUCKET_HTTPS=false
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket
# Path to the WAR file
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
#GITBUCKET_PREFIX=
# Other Java option
#GITBUCKET_JVM_OPTS=

View File

@@ -0,0 +1,109 @@
#!/bin/bash
#
# /etc/rc.d/init.d/gitbucket
#
# Starts the GitBucket server
#
# chkconfig: 345 60 40
# description: Run GitBucket server
# processname: java
# Source function library
. /etc/rc.d/init.d/functions
# Default values
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
# Location of the log and PID file
LOG_FILE=/var/log/gitbucket/run.log
PID_FILE=/var/run/gitbucket.pid
# Default return value
RETVAL=0
start() {
echo -n $"Starting GitBucket server: "
# Compile statup parameters
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
if [ $GITBUCKET_PREFIX ]; then
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
fi
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
if [ $GITBUCKET_HTTPS ]; then
START_OPTS="${START_OPTS} --https=true"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup"
else
failure "GitBucket startup"
fi
echo
return $RETVAL
}
stop() {
echo -n $"Stopping GitBucket server: "
# Run the Java process
kill $(cat $PID_FILE 2>/dev/null) >>$LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
rm -f $PID_FILE
success "GitBucket stopping"
else
failure "GitBucket stopping"
fi
echo
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL

View File

@@ -0,0 +1,44 @@
Name: gitbucket
Summary: Github clone written with Scala.
Version: 1.6
Release: 1%{?dist}
License: Apache
URL: https://github.com/takezoe/gitbucket
Group: System/Servers
Source0: %{name}.war
Source1: %{name}.init
Source2: %{name}.conf
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
BuildArch: noarch
Requires: java >= 1.7
%description
GitBucket is the easily installable Github clone written with Scala.
%install
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
%{__mkdir_p} %{buildroot}{%{_sysconfdir}/{init.d,sysconfig},%{_datarootdir}/%{name}/lib,%{_sharedstatedir}/%{name},%{_localstatedir}/log/%{name}}
%{__install} -m 0644 %{SOURCE0} %{buildroot}%{_datarootdir}/%{name}/lib
%{__install} -m 0755 %{SOURCE1} %{buildroot}%{_sysconfdir}/init.d/%{name}
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%clean
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
%files
%defattr(-,root,root,-)
%{_datarootdir}/%{name}/lib/%{name}.war
%{_sysconfdir}/init.d/%{name}
%config %{_sysconfdir}/sysconfig/%{name}
%{_localstatedir}/log/%{name}/run.log
%changelog
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
- First build.

1
project/build.properties Normal file
View File

@@ -0,0 +1 @@
sbt.version=0.12.3

View File

@@ -10,7 +10,7 @@ object MyBuild extends Build {
val Organization = "jp.sf.amateras"
val Name = "gitbucket"
val Version = "0.0.1"
val ScalaVersion = "2.10.1"
val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.2.1"
lazy val project = Project (
@@ -25,27 +25,31 @@ object MyBuild extends Build {
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
scalacOptions := Seq("-deprecation"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.4",
"org.json4s" %% "json4s-jackson" % "3.2.5",
"jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.3.0",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.2.5",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"com.typesafe.slick" %% "slick" % "1.0.1",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.3.171",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
"com.h2database" % "h2" % "1.3.173",
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
"junit" % "junit" % "4.11" % "test"
),
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*)
)
}
}

View File

@@ -5,3 +5,5 @@ addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")

View File

@@ -1,30 +1,50 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException;
import java.net.URL;
import java.security.ProtectionDomain;
public class JettyLauncher {
public static void main(String[] args) throws Exception {
String host = null;
int port = 8080;
String contextPath = "/";
boolean forceHttps = false;
for(String arg: args){
if(arg.startsWith("--") && arg.contains("=")){
for(String arg: args) {
if(arg.startsWith("--") && arg.contains("=")) {
String[] dim = arg.split("=");
if(dim.length >= 2){
if(dim[0].equals("--port")){
if(dim.length >= 2) {
if(dim[0].equals("--host")) {
host = dim[1];
} else if(dim[0].equals("--port")) {
port = Integer.parseInt(dim[1]);
} else if(dim[0].equals("--prefix")){
} else if(dim[0].equals("--prefix")) {
contextPath = dim[1];
} else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
forceHttps = true;
}
}
}
}
Server server = new Server(port);
WebAppContext context = new WebAppContext();
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
Server server = new Server();
HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
if(host != null) {
connector.setHost(host);
}
connector.setMaxIdleTime(1000 * 60 * 60);
connector.setSoLingerTime(-1);
connector.setPort(port);
server.addConnector(connector);
WebAppContext context = new WebAppContext();
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
context.setContextPath(contextPath);
@@ -37,3 +57,19 @@ public class JettyLauncher {
server.join();
}
}
class HttpsSupportConnector extends SelectChannelConnector {
private boolean forceHttps;
public HttpsSupportConnector(boolean forceHttps) {
this.forceHttps = forceHttps;
}
@Override
public void customize(final EndPoint endpoint, final Request request) throws IOException {
if (this.forceHttps) {
request.setScheme("https");
super.customize(endpoint, request);
}
}
}

View File

@@ -0,0 +1,5 @@
ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;

View File

@@ -15,15 +15,16 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator =>
case class AccountNewForm(userName: String, password: String,mailAddress: String,
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
case class AccountEditForm(password: Option[String], mailAddress: String,
case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
@@ -31,6 +32,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
@@ -84,6 +86,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getAccountByUserName(userName).map { account =>
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
url = form.url))
@@ -106,7 +109,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){
createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound

View File

@@ -9,7 +9,6 @@ import org.eclipse.jgit.api.Git
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.PersonIdent
import scala.Some
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
@@ -74,8 +73,7 @@ trait CreateRepositoryControllerBase extends ControllerBase {
JGitUtil.initRepository(gitdir)
if(form.createReadme){
val tmpdir = getInitRepositoryDir(form.owner, form.name)
try {
FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
@@ -94,12 +92,9 @@ trait CreateRepositoryControllerBase extends ControllerBase {
val git = Git.open(tmpdir)
git.add.addFilepattern("README.md").call
git.commit
.setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
.setMessage("Initial commit").call
git.push.call
} finally {
FileUtils.deleteDirectory(tmpdir)
}
}

View File

@@ -87,10 +87,6 @@ trait DashboardControllerBase extends ControllerBase {
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
getRepositoryNamesOfUser(userName).map { repoName =>
(userName, repoName, counts.collectFirst { case (_, repoName, count) => count })
}
dashboard.html.pulls(
pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),

View File

@@ -14,7 +14,7 @@ class IssuesController extends IssuesControllerBase
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with LabelsService with MilestonesService with ActivityService
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class IssueCreateForm(title: String, content: Option[String],
@@ -65,7 +65,7 @@ trait IssuesControllerBase extends ControllerBase {
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
@@ -77,7 +77,7 @@ trait IssuesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
(getCollaborators(owner, name) :+ owner).sorted,
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
@@ -155,6 +155,16 @@ trait IssuesControllerBase extends ControllerBase {
}
})
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
}
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
@@ -228,13 +238,13 @@ trait IssuesControllerBase extends ControllerBase {
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
defining(params("value").toInt){ labelId =>
params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
}
}
}
} getOrElse NotFound
})
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
@@ -254,7 +264,7 @@ trait IssuesControllerBase extends ControllerBase {
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName

View File

@@ -4,6 +4,7 @@ import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
import util.Implicits._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
@@ -39,34 +40,44 @@ trait MilestonesControllerBase extends ControllerBase {
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository)
params("milestoneId").toIntOpt.map{ milestoneId =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound
})
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
}
} getOrElse NotFound
})

View File

@@ -4,6 +4,7 @@ import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticat
import util.Directory._
import util.Implicits._
import util.ControlUtil._
import util.FileUtil._
import service._
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
@@ -15,7 +16,6 @@ import org.eclipse.jgit.api.MergeCommand.FastForwardMode
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import scala.Some
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo
@@ -24,7 +24,7 @@ class PullRequestsController extends PullRequestsControllerBase
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
val pullRequestForm = mapping(
@@ -63,7 +63,9 @@ trait PullRequestsControllerBase extends ControllerBase {
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
@@ -71,102 +73,102 @@ trait PullRequestsControllerBase extends ControllerBase {
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getComments(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
} getOrElse NotFound
}
}
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
pulls.html.mergeguide(
checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
pullreq,
s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
} getOrElse NotFound()
}
}
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
defining(repository.owner, repository.name, params("id").toInt){ case (owner, name, issueId) =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
LockUtil.lock(s"${owner}/${name}/merge"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
val remote = getRepositoryDir(owner, name)
val tmpdir = new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call
withTmpDir(new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")){ tmpdir =>
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call){ git =>
try {
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
// fetch pull request to temporary working repository
val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
git.fetch
.setRemote(getRepositoryDir(owner, name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
git.fetch
.setRemote(getRepositoryDir(owner, name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
// merge pull request
git.checkout.setName(pullreq.branch).call
// merge pull request
git.checkout.setName(pullreq.branch).call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
val result = git.merge
.include(git.getRepository.resolve(pullRequestBranchName))
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false)
.call
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
if(result.getConflicts != null){
throw new RuntimeException("This pull request can't merge automatically.")
}
// merge commit
git.getRepository.writeMergeCommitMsg(
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ form.message)
git.commit
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
.call
// push
git.push.call
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
commits.flatten.foreach { commit =>
if(!existsCommitId(owner, name, commit.id)){
insertCommitId(owner, name, commit.id)
}
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
}
} getOrElse NotFound
}
}
}
} getOrElse NotFound
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
@@ -186,8 +188,11 @@ trait PullRequestsControllerBase extends ControllerBase {
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
} getOrElse {
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}
}
@@ -313,28 +318,21 @@ trait PullRequestsControllerBase extends ControllerBase {
// TODO Are there more quick way?
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
val remote = getRepositoryDir(userName, repositoryName)
val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
if(tmpdir.exists()){
FileUtils.deleteDirectory(tmpdir)
}
withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call
try {
git.checkout.setName(branch).call
git.checkout.setName(branch).call
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
val result = git.merge
.include(git.getRepository.resolve("FETCH_HEAD"))
.setCommit(false).call
val result = git.merge
.include(git.getRepository.resolve("FETCH_HEAD"))
.setCommit(false).call
result.getConflicts != null
} finally {
git.getRepository.close
FileUtils.deleteDirectory(tmpdir)
result.getConflicts != null
}
}
}
}

View File

@@ -137,13 +137,16 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
callWebHook(repository.owner, repository.name,
WebHookPayload(
git,
"refs/heads/" + repository.repository.defaultBranch,
repository,
commits.toList,
getAccountByUserName(repository.owner).get))
val webHookURLs = getWebHookURLs(repository.owner, repository.name)
if(webHookURLs.nonEmpty){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(
git,
"refs/heads/" + repository.repository.defaultBranch,
repository,
commits.toList,
getAccountByUserName(repository.owner).get))
}
flash += "info" -> "Test payload deployed!"
}
@@ -185,6 +188,8 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None

View File

@@ -11,6 +11,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import java.util.zip.{ZipEntry, ZipOutputStream}
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator
@@ -37,7 +38,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository")(referrersOnly {
fileList(_)
})
/**
* Displays the file list of the specified path and branch.
*/
@@ -49,13 +50,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.getOrElse("page", "1").toInt
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
@@ -93,17 +94,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(raw){
// Download
val mimeType = FileUtil.getMimeType(path)
val bytes = JGitUtil.getContent(git, objectId, false).get
contentType = if(mimeType == "application/octet-stream" && FileUtil.isText(bytes)){
"text/plain"
} else {
mimeType
defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
//response.setHeader("Content-Disposition", s"inline; filename=${FileUtil.getFileName(path)}")
bytes
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -127,7 +121,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
})
/**
* Displays details of the specified commit.
*/
@@ -145,7 +139,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
})
/**
* Displays branches.
*/
@@ -172,7 +166,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
val name = params("name")
if(name.endsWith(".zip")){
val revision = name.replaceFirst("\\.zip$", "")
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
@@ -180,25 +174,35 @@ trait RepositoryViewerControllerBase extends ControllerBase {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
// clone the repository
val cloneDir = new File(workDir, revision)
using(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir)
.call){ git =>
// checkout the specified revision
git.checkout.setName(revision).call
}
// remove .git
FileUtils.deleteDirectory(new File(cloneDir, ".git"))
// create zip file
val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
FileUtil.createZipFile(zipFile, cloneDir)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new TreeWalk(git.getRepository)){ walk =>
val reader = walk.getObjectReader
val objectId = new MutableObjectId
using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
walk.addTree(revCommit.getTree)
walk.setRecursive(true)
while(walk.next){
val name = walk.getPathString
val mode = walk.getFileMode(0)
if(mode != FileMode.TREE){
walk.getObjectId(objectId, 0)
val entry = new ZipEntry(name)
val loader = reader.open(objectId)
entry.setSize(loader.getSize)
out.putNextEntry(entry)
loader.copyTo(out)
}
}
}
}
}
contentType = "application/octet-stream"
zipFile
} else {
@@ -262,4 +266,4 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
}
}

View File

@@ -3,6 +3,7 @@ package app
import service._
import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.StringUtil._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
@@ -47,7 +48,7 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
redirect(urlEncode(redirectUrl).replaceAll("%2F", "/"))
}
}.getOrElse {
redirect("/")

View File

@@ -21,7 +21,9 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean())))
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(

View File

@@ -12,10 +12,12 @@ class UserManagementController extends UserManagementControllerBase
trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with AdminAuthenticator =>
case class NewUserForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
case class EditUserForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
case class EditUserForm(userName: String, password: Option[String], fullName: String,
mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
@@ -27,6 +29,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
@@ -36,6 +39,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
@@ -44,7 +48,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text())))
@@ -71,7 +75,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
})
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
@@ -86,6 +90,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url))
@@ -137,4 +142,4 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
getAccountByUserName(params("userName")).isDefined
})
}
}

View File

@@ -1,34 +1,39 @@
package app
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
import util._
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.FlashMapSupport
import service.WikiService.WikiPageInfo
import scala.Some
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text()))
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply)
val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required)))
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository =>
@@ -58,36 +63,60 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.")
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val commitId = params("commitId").split("\\.\\.\\.")
val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/}")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse("")).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
}
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
})
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -95,25 +124,26 @@ trait WikiControllerBase extends ControllerBase {
})
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, context.loginAccount.get, form.message.getOrElse(""))
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), None)
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
}
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
val account = context.loginAccount.get
deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
defining(context.loginAccount.get){ loginAccount =>
deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -131,9 +161,11 @@ trait WikiControllerBase extends ControllerBase {
})
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
contentType = "application/octet-stream"
content
val path = multiParams("splat").head
getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
} getOrElse NotFound
})
@@ -153,5 +185,22 @@ trait WikiControllerBase extends ControllerBase {
}
}
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = {
optionIf(targetWikiPage.nonEmpty){
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
}
}
}
}
private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = {
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
}
}
}
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
}

View File

@@ -4,6 +4,7 @@ import scala.slick.driver.H2Driver.simple._
object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey)
def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD")
def isAdmin = column[Boolean]("ADMINISTRATOR")
@@ -13,11 +14,12 @@ object Accounts extends Table[Account]("ACCOUNT") {
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE")
def groupAccount = column[Boolean]("GROUP_ACCOUNT")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
}
case class Account(
userName: String,
fullName: String,
mailAddress: String,
password: String,
isAdmin: Boolean,

View File

@@ -9,9 +9,12 @@ import model.GroupMember
import scala.Some
import model.Account
import util.LDAPUtil
import org.slf4j.LoggerFactory
trait AccountService {
private val logger = LoggerFactory.getLogger(classOf[AccountService])
def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
if(settings.ldapAuthentication){
ldapAuthentication(settings, userName, password)
@@ -37,11 +40,14 @@ trait AccountService {
// Create or update account by LDAP information
getAccountByUserName(userName) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
case None => createAccount(userName, "", mailAddress, false, None)
case None => createAccount(userName, "", userName, mailAddress, false, None)
}
getAccountByUserName(userName)
}
case Left(errorMessage) => defaultAuthentication(userName, password)
case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
defaultAuthentication(userName, password)
}
}
}
@@ -53,10 +59,11 @@ trait AccountService {
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
Accounts insert Account(
userName = userName,
password = password,
fullName = fullName,
mailAddress = mailAddress,
isAdmin = isAdmin,
url = url,
@@ -69,9 +76,10 @@ trait AccountService {
def updateAccount(account: Account): Unit =
Accounts
.filter { a => a.userName is account.userName.bind }
.map { a => a.password ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
.map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
.update (
account.password,
account.fullName,
account.mailAddress,
account.isAdmin,
account.url,
@@ -89,6 +97,7 @@ trait AccountService {
Accounts insert Account(
userName = groupName,
password = "",
fullName = groupName,
mailAddress = groupName + "@devnull",
isAdmin = false,
url = url,
@@ -123,3 +132,5 @@ trait AccountService {
.list
}
object AccountService extends AccountService

View File

@@ -109,11 +109,26 @@ trait ActivityService {
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"delete_tag",
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_branch",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"delete_branch",
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
None,
currentDate)
@@ -141,7 +156,13 @@ trait ActivityService {
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId)
}
def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
def getAllCommitIds(userName: String, repositoryName: String): List[String] =
Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined

View File

@@ -49,12 +49,8 @@ trait IssuesService {
* @return the count of the search result
*/
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): Int = {
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
(searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
}
repos: (String, String)*): Int =
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
*
@@ -253,6 +249,9 @@ trait IssuesService {
}
.update (content, currentDate)
def deleteComment(commentId: Int) =
IssueComments filter (_.byPrimaryKey(commentId)) delete
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
@@ -354,10 +353,10 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
param(request, "milestone").map(_ match {
param(request, "milestone").map{
case "none" => None
case x => Some(x.toInt)
}),
case x => x.toIntOpt
},
param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),

View File

@@ -9,13 +9,10 @@ trait PullRequestService { self: IssuesService =>
import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
defining(getIssue(owner, repository, issueId.toString)){ issue =>
if(issue.isDefined){
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
case Some(pullreq) => Some((issue.get, pullreq))
case None => None
}
} else None
getIssue(owner, repository, issueId.toString).flatMap{ issue =>
Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
pullreq => (issue, pullreq)
}
}
def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
@@ -54,4 +51,4 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int)
}
}

View File

@@ -201,13 +201,10 @@ trait RepositoryService { self: AccountService =>
}
}
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories).filter { t =>
Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.list.length
}.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[String] =

View File

@@ -28,5 +28,10 @@ trait RequestCache {
}
}
def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
context.cache(s"account.${mailAddress}"){
new AccountService {}.getAccountByMailAddress(mailAddress)
}
}
}

View File

@@ -18,6 +18,8 @@ trait SystemSettingsService {
smtp.user.foreach(props.setProperty(SmtpUser, _))
smtp.password.foreach(props.setProperty(SmtpPassword, _))
smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
}
}
props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
@@ -52,7 +54,9 @@ trait SystemSettingsService {
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None)))
getOptionValue[Boolean](props, SmtpSsl, None),
getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, SmtpFromName, None)))
} else {
None
},
@@ -100,7 +104,9 @@ object SystemSettingsService {
port: Option[Int],
user: Option[String],
password: Option[String],
ssl: Option[Boolean])
ssl: Option[Boolean],
fromAddress: Option[String],
fromName: Option[String])
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -113,6 +119,8 @@ object SystemSettingsService {
private val SmtpUser = "smtp.user"
private val SmtpPassword = "smtp.password"
private val SmtpSsl = "smtp.ssl"
private val SmtpFromAddress = "smtp.from_address"
private val SmtpFromName = "smtp.from_name"
private val LdapAuthentication = "ldap_authentication"
private val LdapHost = "ldap.host"
private val LdapPort = "ldap.port"

View File

@@ -29,23 +29,21 @@ trait WebHookService {
def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, payload: WebHookPayload): Unit = {
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
val json = write(payload)
val httpClient = new DefaultHttpClient()
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl =>
val f = future {

View File

@@ -4,8 +4,11 @@ import java.io.File
import java.util.Date
import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
import util.{Directory, JGitUtil, LockUtil}
import util.{StringUtil, Directory, JGitUtil, LockUtil}
import util.ControlUtil._
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.eclipse.jgit.diff.DiffFormatter
import org.eclipse.jgit.api.errors.PatchApplyException
object WikiService {
@@ -16,8 +19,9 @@ object WikiService {
* @param content the page content
* @param committer the last committer
* @param time the last modified time
* @param id the latest commit id
*/
case class WikiPageInfo(name: String, content: String, committer: String, time: Date)
case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
/**
* The model for wiki page history.
@@ -40,7 +44,7 @@ trait WikiService {
if(!dir.exists){
try {
JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit")
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
@@ -56,7 +60,7 @@ trait WikiService {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
}
}
}
@@ -89,12 +93,63 @@ trait WikiService {
.sortBy(x => x)
}
}
/**
* Reverts specified changes.
*/
def revertWikiPage(owner: String, repository: String, from: String, to: String,
committer: model.Account, pageName: Option[String]): Boolean = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
// clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository)
using(Git.open(workDir)){ git =>
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
import scala.collection.JavaConverters._
val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
pageName match {
case Some(x) => diff.getNewPath == x + ".md"
case None => true
}
}
val patch = using(new java.io.ByteArrayOutputStream()){ out =>
val formatter = new DiffFormatter(out)
formatter.setRepository(git.getRepository)
formatter.format(diffs.asJava)
new String(out.toByteArray, "UTF-8")
}
try {
git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call
git.add.addFilepattern(".").call
git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match {
case Some(x) => s"Revert ${from} ... ${to} on ${x}"
case None => s"Revert ${from} ... ${to}"
}).call
git.push.call
true
} catch {
case ex: PatchApplyException => false
}
}
}
}
}
/**
* Save the wiki page.
*/
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Option[String] = {
content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
@@ -104,6 +159,10 @@ trait WikiService {
// write as file
using(Git.open(workDir)){ git =>
defining(new File(workDir, newPageName + ".md")){ file =>
// new page
val created = !file.exists
// created or updated
val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
FileUtils.writeStringToFile(file, content, "UTF-8")
git.add.addFilepattern(file.getName).call
@@ -116,7 +175,18 @@ trait WikiService {
// commit and push
optionIf(added || deleted){
defining(git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call){ commit =>
defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
.setMessage(if(message.trim.length == 0){
if(deleted){
s"Rename ${currentPageName} to ${newPageName}"
} else if(created){
s"Created ${newPageName}"
} else {
s"Updated ${newPageName}"
}
} else {
message
}).call){ commit =>
git.push.call
Some(commit.getName)
}
@@ -144,7 +214,7 @@ trait WikiService {
git.rm.addFilepattern(pageName + ".md").call
// commit and push
git.commit.setAuthor(committer, mailAddress).setMessage(message).call
git.commit.setCommitter(committer, mailAddress).setMessage(message).call
git.push.call
}
}
@@ -164,4 +234,4 @@ trait WikiService {
}
}
}
}

View File

@@ -1,9 +1,9 @@
package servlet
import java.io.File
import java.sql.Connection
import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent
import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import util.Directory._
@@ -50,6 +50,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
@@ -73,7 +74,8 @@ object AutoUpdate {
},
Version(1, 2),
Version(1, 1),
Version(1, 0)
Version(1, 0),
Version(0, 0)
)
/**
@@ -108,28 +110,29 @@ object AutoUpdate {
}
/**
* Start H2 database and update schema automatically.
* Update database schema automatically in the context initializing.
*/
class AutoUpdateListener extends org.h2.server.web.DbStarter {
class AutoUpdateListener extends ServletContextListener {
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
org.h2.Driver.load()
event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
super.contextInitialized(event)
logger.debug("H2 started")
logger.debug("Start schema update")
defining(getConnection()){ conn =>
defining(getConnection(event.getServletContext)){ conn =>
try {
defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){
logger.debug("No update")
} else if(!versions.contains(currentVersion)){
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
} else {
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
conn.commit()
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
}
}
} catch {
@@ -142,5 +145,15 @@ class AutoUpdateListener extends org.h2.server.web.DbStarter {
}
logger.debug("End schema update")
}
}
def contextDestroyed(sce: ServletContextEvent): Unit = {
// Nothing to do.
}
private def getConnection(servletContext: ServletContext): Connection =
DriverManager.getConnection(
servletContext.getInitParameter("db.url"),
servletContext.getInitParameter("db.user"),
servletContext.getInitParameter("db.password"))
}

View File

@@ -15,6 +15,7 @@ import util.Implicits._
import service._
import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
/**
* Provides Git repository via HTTP.
@@ -84,52 +85,76 @@ class CommitLogHook(owner: String, repository: String, userName: String, baseURL
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
val commits = command.getType match {
case ReceiveCommand.Type.DELETE => Nil
case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
}
val refName = command.getRefName.split("/")
// apply issue comment
val newCommits = commits.flatMap { commit =>
if(!existsCommitId(owner, repository, commit.id)){
insertCommitId(owner, repository, commit.id)
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
}
val branchName = refName.drop(2).mkString("/")
// Extract new commit and apply issue comment
val newCommits = if(commits.size > 1000){
val existIds = getAllCommitIds(owner, repository)
commits.flatMap { commit =>
optionIf(!existIds.contains(commit.id)){
createIssueComment(commit)
Some(commit)
}
Some(commit)
} else None
}.toList
}
} else {
commits.flatMap { commit =>
optionIf(!existsCommitId(owner, repository, commit.id)){
createIssueComment(commit)
Some(commit)
}
}
}
// batch insert all new commit id
insertAllCommitIds(owner, repository, newCommits.map(_.id))
// record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => {
recordCreateBranchActivity(owner, repository, userName, refName(2))
recordPushActivity(owner, repository, userName, refName(2), newCommits)
}
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName)
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName)
case _ =>
}
} else if(refName(1) == "tags"){
command.getType match {
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits)
case _ =>
}
}
// call web hook
val payload = WebHookPayload(
git,
command.getRefName,
getRepository(owner, repository, baseURL).get,
newCommits,
getAccountByUserName(owner).get)
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
val payload = WebHookPayload(
git,
command.getRefName,
getRepository(owner, repository, baseURL).get,
newCommits,
getAccountByUserName(owner).get)
callWebHook(owner, repository, payload)
callWebHook(owner, repository, webHookURLs, payload)
}
}
}
// update repository last modified time.
updateLastActivityDate(owner, repository)
}
private def createIssueComment(commit: CommitInfo) = {
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
val issueId = matchData.group(2)
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}

View File

@@ -1,9 +1,8 @@
package util
import org.apache.commons.io.{IOUtils, FileUtils}
import org.apache.commons.io.FileUtils
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
import util.ControlUtil._
object FileUtil {
@@ -16,29 +15,39 @@ object FileUtil {
}
}
def getContentType(name: String, bytes: Array[Byte]): String = {
defining(getMimeType(name)){ mimeType =>
if(mimeType == "application/octet-stream" && isText(bytes)){
"text/plain"
} else {
mimeType
}
}
}
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
def createZipFile(dest: File, dir: File): Unit = {
def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
dir.listFiles.map { file =>
if(file.isFile){
out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
out.write(FileUtils.readFileToByteArray(file))
out.closeArchiveEntry
} else if(file.isDirectory){
addDirectoryToZip(out, file, path + "/" + file.getName)
}
}
}
using(new ZipArchiveOutputStream(dest)){ out =>
addDirectoryToZip(out, dir, dir.getName)
}
}
// def createZipFile(dest: File, dir: File): Unit = {
// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
// dir.listFiles.map { file =>
// if(file.isFile){
// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
// out.write(FileUtils.readFileToByteArray(file))
// out.closeArchiveEntry
// } else if(file.isDirectory){
// addDirectoryToZip(out, file, path + "/" + file.getName)
// }
// }
// }
//
// using(new ZipArchiveOutputStream(dest)){ out =>
// addDirectoryToZip(out, dir, dir.getName)
// }
// }
def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
if(i >= 0) path.substring(i + 1) else path
@@ -50,4 +59,14 @@ object FileUtil {
case _ => ""
}
}
def withTmpDir[A](dir: File)(action: File => A): A = {
if(dir.exists()){
FileUtils.deleteDirectory(dir)
}
try{
action(dir)
}finally{
FileUtils.deleteDirectory(dir)
}
}
}

View File

@@ -41,6 +41,12 @@ object Implicits {
}
sb.toString
}
def toIntOpt: Option[Int] = try {
Option(Integer.parseInt(value))
} catch {
case e: NumberFormatException => None
}
}
implicit class RichRequest(request: HttpServletRequest){
@@ -69,4 +75,4 @@ object Implicits {
}
}
}
}

View File

@@ -43,8 +43,10 @@ object JGitUtil {
* @param message the last commit message
* @param commitId the last commit id
* @param committer the last committer name
* @param mailAddress the committer's mail address
*/
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, committer: String)
case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
committer: String, mailAddress: String)
/**
* The commit data.
@@ -206,7 +208,8 @@ object JGitUtil {
commits(path).getCommitterIdent.getWhen,
commits(path).getShortMessage,
commits(path).getName,
commits(path).getCommitterIdent.getName)
commits(path).getCommitterIdent.getName,
commits(path).getCommitterIdent.getEmailAddress)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
@@ -453,10 +456,11 @@ object JGitUtil {
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
revstr: String = ""): Option[(ObjectId, String)] = {
Seq(
if(revstr.isEmpty) repository.repository.defaultBranch else revstr,
repository.branchList.head
).map { rev =>
(git.getRepository.resolve(rev), rev)
Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr),
repository.branchList.headOption
).flatMap {
case Some(rev) => Some((git.getRepository.resolve(rev), rev))
case None => None
}.find(_._1 != null)
}

View File

@@ -28,7 +28,7 @@ object LDAPUtil {
withConnection(conn) { conn =>
findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
case None => Left("User does not exist")
case None => Left("User does not exist.")
}
}
}

View File

@@ -9,6 +9,7 @@ import app.Context
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database
import SystemSettingsService.Smtp
import _root_.util.ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
@@ -68,25 +69,33 @@ class Mailer(private val smtp: Smtp) extends Notifier {
val database = Database(context.request.getServletContext)
val f = future {
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
email.setFrom("notifications@gitbucket.com", context.loginAccount.get.userName)
email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
// TODO Can we use the Database Session in other than Transaction Filter?
database withSession {
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
recipients(issue) {
email.getToAddresses.clear
email.addTo(_).send
defining(
s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
smtp.fromAddress
.map (_ -> smtp.fromName.orNull)
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
}
}
}

View File

@@ -9,7 +9,7 @@ trait Validations {
*/
def identifier: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")

View File

@@ -13,17 +13,31 @@ trait AvatarImageProvider { self: RequestCache =>
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
val src = if(mailAddress.isEmpty){
// by user name
getAccountByUserName(userName).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
s"""${context.path}/_unknown/_avatar"""
}
} getOrElse {
if(mailAddress.nonEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
} else {
// by mail address
getAccountByMailAddress(mailAddress).map { account =>
if(account.image.isEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/${account.userName}/_avatar"""
}
} getOrElse {
if(getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}"""
} else {
s"""${context.path}/_unknown/_avatar"""
}
}
}

View File

@@ -15,10 +15,12 @@ trait LinkConverter { self: RequestCache =>
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
// convert issue id to link
.replaceBy(("(^|\\W)" + issueIdPrefix + "(\\d+)(\\W|$)").r){ m =>
if(getIssue(repository.owner, repository.name, m.group(2)).isDefined){
Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
} else {
Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
getIssue(repository.owner, repository.name, m.group(2)) match {
case Some(issue) if(issue.isPullRequest)
=> Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
case Some(_) => Some(s"""${m.group(1)}<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(2)}">#${m.group(2)}</a>${m.group(3)}""")
case None => Some(s"""${m.group(1)}#${m.group(2)}${m.group(3)}""")
}
}
// convert @username to link

View File

@@ -1,12 +1,14 @@
package view
import util.StringUtil
import util.ControlUtil._
import util.Directory._
import org.parboiled.common.StringUtils
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._
import service.RequestCache
import service.{RequestCache, WikiService}
object Markdown {
@@ -29,7 +31,7 @@ object Markdown {
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean) extends LinkRenderer {
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -40,8 +42,14 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} else {
(text, text)
}
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
new Rendering(url, label)
if(getWikiPage(repository.owner, repository.name, page).isDefined){
new Rendering(url, label)
} else {
new Rendering(url, label).withAttribute("class", "absent")
}
} catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
}

View File

@@ -9,12 +9,12 @@ import service.RequestCache
* Provides helper methods for Twirl templates.
*/
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
*/
def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/**
* Format java.util.Date to "yyyy-MM-dd".
*/
@@ -57,16 +57,29 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
value
}
import scala.util.matching.Regex
import scala.util.matching.Regex._
implicit class RegexReplaceString(s: String) {
def replaceAll(pattern: String, replacer: (Match) => String): String = {
pattern.r.replaceAllIn(s, replacer)
}
}
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""")
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""<a href="${context.path}/$$1">$$1</a>""")
)
/**
* URL encode except '/'.
*/
def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
def urlEncode(value: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
@@ -87,6 +100,15 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
*/
def assets(implicit context: app.Context): String = s"${context.path}/assets"
/**
* Generates the link to the account page.
*/
def user(userName: String, mailAddress: String, styleClass: String = "")(implicit context: app.Context): Html = {
getAccountByMailAddress(mailAddress).map { account =>
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${userName}</a>""")
} getOrElse Html(userName)
}
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/**

View File

@@ -13,7 +13,7 @@
<div class="span6">
@if(account.isEmpty){
<fieldset>
<label for="userName" class="strong">User name</label>
<label for="userName" class="strong">Username:</label>
<input type="text" name="userName" id="userName" value=""/>
<span id="error-userName" class="error"></span>
</fieldset>
@@ -23,27 +23,33 @@
<label for="password" class="strong">
Password
@if(account.nonEmpty){
(Input to change password)
(input to change password)
}
:
</label>
<input type="password" name="password" id="password" value=""/>
<span id="error-password" class="error"></span>
</fieldset>
}
<fieldset>
<label for="mailAddress" class="strong">Mail Address</label>
<label for="fullName" class="strong">Full Name:</label>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
<span id="error-fullName" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label for="url" class="strong">URL (Optional)</label>
<label for="url" class="strong">URL (optional):</label>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
<span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>

View File

@@ -7,7 +7,8 @@
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
<div class="account-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){

View File

@@ -13,13 +13,13 @@
<!--====================================================================-->
<label class="strong">Account registration</label>
<fieldset>
<label>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> - Users can create account by themselves.
<span class="strong">Allow</span> - Users can create accounts by themselves.
</label>
<label>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> - Only administrators can create account.
<span class="strong">Deny</span> - Only administrators can create accounts.
</label>
</fieldset>
<!--====================================================================-->
@@ -28,9 +28,9 @@
<hr>
<label class="strong">Services</label>
<fieldset>
<label>
<label class="checkbox">
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
Gravatar
Use Gravatar for Profile-Images
</label>
</fieldset>
<!--====================================================================-->
@@ -39,7 +39,7 @@
<hr>
<label class="strong">Authentication</label>
<fieldset>
<label>
<label class="checkbox">
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
LDAP
</label>
@@ -101,7 +101,7 @@
<hr>
<label class="strong">Notification email</label>
<fieldset>
<label>
<label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
@@ -140,6 +140,18 @@
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromAddress">FROM Address</label>
<div class="controls">
<input type="text" id="fromAddress" name="smtp.fromAddress" value="@settings.smtp.map(_.fromAddress)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="fromName">FROM Name</label>
<div class="controls">
<input type="text" id="fromName" name="smtp.fromName" value="@settings.smtp.map(_.fromName)"/>
</div>
</div>
</div>
</div>
</div>

View File

@@ -8,12 +8,16 @@
<div class="span7">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<span id="error-groupName" class="error"></span>
<div>
<span id="error-groupName" class="error"></span>
</div>
<input type="text" name="groupName" id="groupName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
<fieldset>
<label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 300px;" value="@account.map(_.url)"/>
</fieldset>
<fieldset>

View File

@@ -6,8 +6,10 @@
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName" class="strong">Username</label>
<span id="error-userName" class="error"></span>
<label for="userName" class="strong">Username:</label>
<div>
<span id="error-userName" class="error"></span>
</div>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
</fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){
@@ -17,28 +19,42 @@
@if(account.isDefined){
(Input to change password)
}
:
</label>
<span id="error-password" class="error"></span>
<div>
<span id="error-password" class="error"></span>
</div>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
}
<fieldset>
<label for="mailAddress" class="strong">Mail Address</label>
<span id="error-mailAddress" class="error"></span>
<label for="fullName" class="strong">Full Name:</label>
<div>
<span id="error-fullName" class="error"></span>
</div>
<input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/>
</fieldset>
<fieldset>
<label for="mailAddress" class="strong">Mail Address:</label>
<div>
<span id="error-mailAddress" class="error"></span>
</div>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
</fieldset>
<fieldset>
<label class="strong">User Type</label>
<label for="userType_Normal">
<label class="strong">User Type:</label>
<label class="radio" for="userType_Normal">
<input type="radio" name="isAdmin" id="userType_Normal" value="false"@if(account.isEmpty || !account.get.isAdmin){ checked}/> Normal
</label>
<label for="userType_Admin">
<label class="radio" for="userType_Admin">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label>
</fieldset>
<fieldset>
<label class="strong">URL (Optional)</label>
<span id="error-url" class="error"></span>
<label class="strong">URL (Optional):</label>
<div>
<span id="error-url" class="error"></span>
</div>
<input type="text" name="url" id="url" style="width: 400px;" value="@account.map(_.url)"/>
</fieldset>
</div>
@@ -55,4 +71,4 @@
</fieldset>
</form>
}
}
}

View File

@@ -1,17 +1,24 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
@if(repository.commitCount > 0){
<div class="pull-right">
<div class="input-prepend">
<a href="@path/@repository.owner/@repository.name/fork" class="btn" style="margin-bottom: 10px;">Fork</a>
<span class="add-on"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
</div>
</div>
</div>
}
<div class="head">
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
<i class="icon-lock"></i>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){
<div class="forked">

View File

@@ -16,7 +16,9 @@
case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
case "create_branch" => simpleActivity(activity, "activity-branch.png")
case "delete_branch" => simpleActivity(activity, "activity-delete.png")
case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "delete_tag" => simpleActivity(activity, "activity-delete.png")
case "fork" => simpleActivity(activity, "activity-fork.png")
case "push" => customActivity(activity, "activity-commit.png"){
<div class="small activity-message">

View File

@@ -1,5 +1,5 @@
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean,
style: String = "", placeholder: String = "Leave a comment")(implicit context: app.Context)
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="tabbable">
@@ -27,6 +27,10 @@
<script src="@assets/google-code-prettify/prettify.js"></script>
<script>
$(function(){
@if(elastic){
$('#content').elastic();
}
$('#preview').click(function(){
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {

View File

@@ -15,6 +15,7 @@
</label>
}
<input type="hidden" name="fileId" value=""/>
@if(account.isEmpty || account.get.image.isEmpty){
<script>
$(function(){
var dropzone = new Dropzone('div#clickable', {
@@ -32,6 +33,7 @@ $(function(){
});
});
</script>
}
<style type="text/css">
div.dz-filename, div.dz-size, div.dz-progress, div.dz-success-mark, div.dz-error-mark, div.dz-error-message {
display: none;

View File

@@ -31,6 +31,12 @@
@userRepositories.map { repository =>
<tr>
<td>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
@@ -57,6 +63,12 @@
@recentRepositories.map { repository =>
<tr>
<td>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
@if(!repository.repository.isPrivate){
<i class="icon-eye-open"></i>
}
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</td>
</tr>

View File

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

View File

@@ -15,12 +15,20 @@
<span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
<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>
}
</span>
</div>
<div class="box-content"class="issue-content" id="commentContent-@comment.commentId">
@markdown(comment.content, repository, false, true)
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
@markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true)
}
} else {
@markdown(comment.content, repository, false, true)
}
</div>
</div>
}
@@ -69,5 +77,18 @@ $(function(){
});
return false;
});
$('i.icon-remove-circle').click(function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
$.post('@url(repository)/issue_comments/delete/' + id,
function(data){
if(data > 0) {
$('#comment-' + id).prev('div.issue-avatar-image').remove();
$('#comment-' + id).remove();
}
});
}
return false;
});
});
</script>

View File

@@ -56,7 +56,7 @@
</div>
</div>
<hr>
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;")
@helper.html.preview(repository, "", false, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true)
</div>
</div>
<div class="pull-right">
@@ -97,7 +97,7 @@ $(function(){
} else {
$('#label-assigned').html($('<span>')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' well be assigned'));
.append(' will be assigned'));
$('a.assign[data-name=' + userName + '] i').attr('class', 'icon-ok');
}
$('input[name=assignedUserName]').val(userName);

View File

@@ -2,11 +2,13 @@
@import context._
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 680px;" id="edit-title" value="@title"/>
<textarea style="width: 680px; height: 100px;" id="edit-content">@content.getOrElse("")</textarea>
<textarea style="width: 680px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
<input type="button" class="btn btn-small" value="Update Issue"/>
<span class="pull-right"><a class="btn btn-small btn-danger" href="#">Cancel</a></span>
<script>
$(function(){
$('#edit-content').elastic();
var callback = function(data){
$('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content);

View File

@@ -28,6 +28,7 @@
<script src="@assets/colorpicker/js/bootstrap-colorpicker.js"></script>
<script src="@assets/google-code-prettify/prettify.js"></script>
<script src="@assets/zclip/ZeroClipboard.min.js"></script>
<script src="@assets/elastic/jquery.elastic.source.js"></script>
</head>
<body>
<form id="search" action="@path/search" method="POST">

View File

@@ -5,7 +5,7 @@
<div style="width: 600px; margin: 10px auto;">
<form id="form" method="post" action="@path/new" validate="true">
<fieldset>
<label for="name" class="strong">Repository name</label>
<label for="name" class="strong">Repository name:</label>
<div class="btn-group" style="margin-bottom: 10px;" id="owner-dropdown">
<button class="btn dropdown-toggle" data-toggle="dropdown">
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
@@ -19,38 +19,38 @@
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
/
<span class="slash">/</span>
<input type="text" name="name" id="name" />
<span id="error-name" class="error"></span>
</fieldset>
<fieldset>
<label for="description" class="strong">Description (optional)</label>
<label for="description" class="strong">Description (optional):</label>
<input type="text" name="description" id="description" style="width: 95%;"/>
</fieldset>
<fieldset class="margin">
<label>
<label class="radio">
<input type="radio" name="isPrivate" value="false" checked>
<span class="strong">Public</span><br>
<span class="strong"><i class="icon-eye-open">&nbsp;</i>&nbsp;Public</span><br>
<div>
<span class="note">All users and guests can read this repository.</span>
<span>All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<label class="radio">
<input type="radio" name="isPrivate" value="true">
<span class="strong">Private</span><br>
<span class="strong"><i class="icon-lock">&nbsp;</i>&nbsp;Private</span><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
<span>Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme">
<label for="createReadme" class="checkbox">
<input type="checkbox" name="createReadme" id="createReadme"/>
<span class="strong">Initialize this repository with a README</span>
<div>
<span class="note">This will allow you to <code>git clone</code> the repository immediately.</span>
<span>This will allow you to <code>git clone</code> the repository immediately.</span>
</div>
</label>
</fieldset>

View File

@@ -11,8 +11,8 @@
@day.map { commit =>
<tr>
<td style="width: 20%;">
@avatar(commit.committer, 20)
<a href="@url(commit.committer)" class="username">@commit.committer</a>
@avatar(commit, 20)
@user(commit.committer, commit.mailAddress, "username")
</td>
<td>@commit.shortMessage</td>
<td style="width: 10%; text-align: right;">

View File

@@ -11,7 +11,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Pull Requests - " + repository.owner + "/" + repository.name){
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("pulls", repository)
<div class="pullreq-info">
<div id="compare-info">
@@ -27,7 +27,7 @@
}
@helper.html.dropdown(originId, "base") {
@originRepository.branchList.map { branch =>
<li><a href="#" class="origin-branch" data-name="@branch">@helper.html.checkicon(branch == originId) @branch</a></li>
<li><a href="#" class="origin-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == originId) @branch</a></li>
}
}
...
@@ -38,7 +38,7 @@
}
@helper.html.dropdown(forkedId, "compare") {
@forkedRepository.branchList.map { branch =>
<li><a href="#" class="forked-branch" data-name="@branch">@helper.html.checkicon(branch == forkedId) @branch</a></li>
<li><a href="#" class="forked-branch" data-name="@encodeRefName(branch)">@helper.html.checkicon(branch == forkedId) @branch</a></li>
}
}
</div>

View File

@@ -10,7 +10,7 @@
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Pull Requests - " + repository.owner + "/" + repository.name){
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("pulls", repository)
<div class="row-fluid">
<div class="span3">

View File

@@ -9,7 +9,7 @@
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("%s - Pull Request #%d - %s/%s".format(issue.title, issue.issueId, repository.owner, repository.name)){
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.header("pulls", repository)
@defining(dayByDayCommits.flatten){ commits =>
<div class="pullreq-info">

View File

@@ -9,12 +9,12 @@
@html.header("code", repository)
@tab(branch, repository, "files")
<div class="head">
<a href="@url(repository)/tree/@branch">@repository.name</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
@if(i == pathList.length - 1){
@section
} else {
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
}
</div>
@@ -24,13 +24,13 @@
<th style="font-weight: normal;">
<div class="pull-left">
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
@user(latestCommit.committer, latestCommit.mailAddress, "username strong")
<span class="muted">@datetime(latestCommit.time)</span>
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div>
<div class="btn-group pull-right">
<a class="btn btn-mini" href="?raw=true">Raw</a>
<a class="btn btn-mini" href="@url(repository)/commits/@branch/@pathList.mkString("/")">History</a>
<a class="btn btn-mini" href="@url(repository)/commits/@encodeRefName(branch)/@pathList.mkString("/")">History</a>
</div>
</th>
</tr>

View File

@@ -16,7 +16,7 @@
@branchInfo.map { case (branchName, latestUpdateDate) =>
<tr>
<td>
<a href="@url(repository)/tree/@branchName">@branchName</a>
<a href="@url(repository)/tree/@encodeRefName(branchName)">@branchName</a>
@*
<a href="#" class="btn btn-danger btn-mini">Delete branch</a>
*@
@@ -28,10 +28,10 @@
@if(repository.repository.defaultBranch == branchName){
Base branch
} else {
<a href="@url(repository)/compare/@{repository.repository.defaultBranch}...@branchName">to @{repository.repository.defaultBranch}</a>
<a href="@url(repository)/compare/@{encodeRefName(repository.repository.defaultBranch)}...@{encodeRefName(branchName)}">to @{repository.repository.defaultBranch}</a>
}
</td>
<td><a href="@url(repository)/archive/@{branchName}.zip">ZIP</a></td>
<td><a href="@url(repository)/archive/@{encodeRefName(branchName)}.zip">ZIP</a></td>
</tr>
}
</table>

View File

@@ -26,7 +26,7 @@
<span class="muted">
<img src="@assets/common/images/branch.png"/>
@branches.zipWithIndex.map { case (branch, i) =>
<a href="@url(repository)/tree/@branch" class="branch" id="branch-@i">@branch</a>
<a href="@url(repository)/tree/@encodeRefName(branch)" class="branch" id="branch-@i">@branch</a>
}
</span>
}
@@ -44,7 +44,7 @@
<tr>
<td>
@avatar(commit, 20)
<a href="@url(commit.committer)" class="username strong">@commit.committer</a>
@user(commit.committer, commit.mailAddress, "username strong")
<span class="muted">@datetime(commit.time)</span>
<div class="pull-right monospace small" style="text-align: right;">
<div>

View File

@@ -11,16 +11,16 @@
@tab(branch, repository, if(pathList.isEmpty) "commits" else "files")
<div class="head">
@if(pathList.isEmpty){
<a href="@url(repository)/tree/@branch">@repository.name</a> / Commit History
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> / Commit History
}
@if(pathList.nonEmpty){
<span class="muted">History for</span>
<a href="@url(repository)/tree/@branch">@repository.name</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
@if(i == pathList.length - 1){
@section
} else {
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
}
}
@@ -49,7 +49,7 @@
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
}
<div class="small">
<a href="@url(commit.committer)" class="username">@commit.committer</a>
@user(commit.committer, commit.mailAddress, "username")
<span class="muted">@datetime(commit.time)</span>
</div>
</div>
@@ -61,6 +61,6 @@
}
<div class="btn-group">
<button class="btn" onclick="location.href='?page=@{page - 1}'"@if(page <= 1){ disabled="true"}>&lt; Newer</button>
<button class="btn" onclick="location.href='?page=@{page + 1}'"@if(!hasNext){ disabled="true"}>Order &gt;</button>
<button class="btn" onclick="location.href='?page=@{page + 1}'"@if(!hasNext){ disabled="true"}>Older &gt;</button>
</div>
}
}

View File

@@ -12,12 +12,12 @@
<div class="head">
<div class="pull-right">
@defining(repository.commitCount){ commitCount =>
<a href="@url(repository)/commits/@branch">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
<a href="@url(repository)/commits/@encodeRefName(branch)">@if(commitCount > 1000){ @commitCount+ } else { @commitCount } @plural(commitCount, "commit")</a>&nbsp;
}
</div>
<a href="@url(repository)/tree/@branch">@repository.name</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@url(repository)/tree/@branch/@pathList.take(i + 1).mkString("/")">@section</a> /
<a href="@url(repository)/tree/@encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
</div>
<div class="box">
@@ -35,7 +35,7 @@
<td colspan="4" class="latest-commit">
<div>
@avatar(latestCommit, 20)
<a href="@url(latestCommit.committer)" class="username strong">@latestCommit.committer</a>
@user(latestCommit.committer, latestCommit.mailAddress, "username strong")
<span class="muted">@datetime(latestCommit.time)</span>
<div class="pull-right align-right monospace">
<a href="@url(repository)/commit/@latestCommit.id" class="commit-id"><span class="muted">latest commit</span> @latestCommit.id.substring(0, 10)</a>
@@ -46,7 +46,7 @@
@if(pathList.size > 0){
<tr>
<td width="16"></td>
<td><a href="@url(repository)@if(pathList.size > 1){/tree/@branch/@pathList.init.mkString("/")}">..</a></td>
<td><a href="@url(repository)@if(pathList.size > 1){/tree/@encodeRefName(branch)/@pathList.init.mkString("/")}">..</a></td>
<td></td>
<td></td>
</tr>
@@ -62,22 +62,25 @@
</td>
<td>
@if(file.isDirectory){
<a href="@url(repository)/tree@{(branch :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
} else {
<a href="@url(repository)/blob@{(branch :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
</td>
<td>@datetime(file.time)</td>
<td><a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a> [<a href="@url(file.committer)">@file.committer</a>]</td>
<td>
<a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a>
[@user(file.committer, file.mailAddress)]
</td>
</tr>
}
</table>
</div>
@readme.map { content =>
<div class="box">
<div id="readme" class="box">
<div class="box-header">README.md</div>
<div class="box-content">@markdown(content, repository, false, false)</div>
</div>
}
}
}

View File

@@ -17,7 +17,7 @@
</span>
} else {
@avatar(repository.repository.originUserName.get, 20)
<span@if(repository.owner == repository.repository.originUserName){ class="highlight"}>
<span class="highlight">
@repository.repository.originUserName / @repository.repository.originRepositoryName
</span>
}

View File

@@ -12,13 +12,13 @@
style = "margin-right: 20px;"
){
@repository.branchList.map { branch =>
<li><a href="@url(repository)/@if(active=="commits"){commits} else {tree}/@branch">@helper.html.checkicon(id == branch) @branch</a></li>
<li><a href="@url(repository)/@if(active=="commits"){commits} else {tree}/@encodeRefName(branch)">@helper.html.checkicon(id == branch) @branch</a></li>
}
}
</li>
}
<li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@id">Files</a></li>
<li@if(active=="commits" ){ class="active"}><a href="@url(repository)/commits/@id">Commits</a></li>
<li@if(active=="files" ){ class="active"}><a href="@url(repository)/tree/@encodeRefName(id)">Files</a></li>
<li@if(active=="commits" ){ class="active"}><a href="@url(repository)/commits/@encodeRefName(id)">Commits</a></li>
<li@if(active=="branches"){ class="active"}><a href="@url(repository)/branches">Branches@if(repository.branchList.length > 0){ <span class="badge">@repository.branchList.length</span>}</a></li>
<li@if(active=="tags" ){ class="active"}><a href="@url(repository)/tags">Tags@if(repository.tags.length > 0){ <span class="badge">@repository.tags.length</span>}</a></li>
<li class="pull-right">
@@ -26,4 +26,9 @@
<input type="text" value="@repository.url" id="repository-url" readonly>
}
</li>
<li class="pull-right" style="margin-right: 8px;">
<div class="pull-right">
<a href="@{url(repository)}/archive/@{encodeRefName(id)}.zip" class="btn"><i class="icon-download-alt"></i>ZIP</a>
</div>
</li>
</ul>

View File

@@ -14,10 +14,10 @@
</tr>
@repository.tags.map { tag =>
<tr>
<td><a href="@url(repository)/tree/@tag.name">@tag.name</a></td>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
<td>@datetime(tag.time)</td>
<td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
<td><a href="@url(repository)/archive/@(tag.name).zip">ZIP</a></td>
<td><a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a></td>
</tr>
}
</table>

View File

@@ -10,11 +10,11 @@
<div class="box-header">Settings</div>
<div class="box-content">
<fieldset>
<label for="description" class="strong">Description</label>
<label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" style="width: 600px;" value="@repository.repository.description"/>
</fieldset>
<fieldset class="margin">
<label for="defaultBranch" class="strong">Default Branch</label>
<label for="defaultBranch" class="strong">Default Branch:</label>
<select name="defaultBranch" id="defaultBranch">
@repository.branchList.map { branch =>
<option value="@branch"@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
@@ -23,26 +23,26 @@
<span class="error" id="error-defaultBranch"></span>
</fieldset>
<fieldset class="margin">
<label>
<label class="radio">
<input type="radio" name="isPrivate" value="false"
@if(!repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled }
>
<span class="strong">Public</span><br>
<div>
<span class="note">All users and guests can read this repository.</span>
<span>All users and guests can read this repository.</span>
</div>
</label>
</fieldset>
<fieldset>
<label>
<label class="radio">
<input type="radio" name="isPrivate" value="true"
@if(repository.repository.isPrivate ){ checked }
@if(repository.repository.parentUserName.isDefined){ disabled }
>
<span class="strong">Private</span><br>
<div>
<span class="note">Only collaborators can read this repository.</span>
<span>Only collaborators can read this repository.</span>
</div>
</label>
</fieldset>
@@ -50,7 +50,7 @@
</div>
@*
<div class="box">
<div class="box-header">Features</div>
<div class="box-header">Features:</div>
<div class="box-content">
<dl>
<dt>

View File

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

View File

@@ -1,10 +1,15 @@
@(pageName: Option[String],
from: String,
to: String,
diffs: Seq[util.JGitUtil.DiffInfo],
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean,
info: Option[Any])(implicit context: app.Context)
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@helper.html.information(info)
@html.header("wiki", repository)
@tab("history", repository)
<ul class="nav nav-tabs">
@@ -23,4 +28,13 @@
</li>
</ul>
@helper.html.diff(diffs, repository, None, None, false)
@if(hasWritePermission){
<div>
@if(pageName.isDefined){
<a href="@url(repository)/wiki/@urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>
} else {
<a href="@url(repository)/wiki/_revert/@from...@to" class="btn">Revert Changes</a>
}
</div>
}
}

View File

@@ -26,6 +26,7 @@
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "")
<input type="text" name="message" value="" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/>
<input type="submit" value="Save" class="btn btn-success">
</form>
}

View File

@@ -35,10 +35,9 @@
@commits.map { commit =>
<tr>
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
<td>@avatar(commit.committer, 20)<a href="@url(commit.committer)">@commit.committer</a></td>
<td>@avatar(commit.committer, 20)&nbsp;<a href="@url(commit.committer)">@commit.committer</a></td>
<td width="80%">
<span class="muted">@datetime(commit.time):</span>
@commit.shortMessage
<span class="muted">@datetime(commit.time):</span>&nbsp;@commit.shortMessage
</td>
</tr>
}

View File

@@ -5,6 +5,10 @@ body {
color: #333;
}
li p {
margin: 0px;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
@@ -256,10 +260,6 @@ hr {
margin-bottom: 4px;
}
span.note {
margin-left: 20px;
}
img.avatar {
border-radius: 4px;
-webkit-border-radius: 4px;
@@ -302,6 +302,13 @@ span.highlight {
background-color: #ffff88;
}
/****************************************************************************/
/* Create Repository */
/****************************************************************************/
.slash {
font-size: 20px;
}
/****************************************************************************/
/* Sign-in form */
/****************************************************************************/
@@ -310,6 +317,20 @@ div.signin-form {
margin: 30px auto;
}
/****************************************************************************/
/* Account page */
/****************************************************************************/
.account-fullname {
font-size: 140%;
font-weight: bold;
}
.account-username {
font-size: 120%;
color: #888888;
font-weight: bold;
}
/****************************************************************************/
/* Stacked side menu */
/****************************************************************************/
@@ -426,7 +447,7 @@ pre.commit-description {
#repository-url {
background-color: white;
cursor: default;
width: 350px;
width: 300px;
}
ul#commit-file-list {
@@ -455,6 +476,10 @@ pre.blob {
padding-left: 20px;
}
#readme .box-content {
padding: 30px;
}
/****************************************************************************/
/* Issues */
/****************************************************************************/
@@ -649,6 +674,10 @@ h1.wiki-title {
margin-top: 0px;
}
a.absent {
color: #c00;
}
/****************************************************************************/
/* Repository Settings */
/****************************************************************************/
@@ -808,4 +837,4 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
-webkit-border-top-right-radius: 4px;
border-top-right-radius: 4px;
-moz-border-radius-topright: 4px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,162 @@
/**
* @name Elastic
* @descripton Elastic is jQuery plugin that grow and shrink your textareas automatically
* @version 1.6.11
* @requires jQuery 1.2.6+
*
* @author Jan Jarfalk
* @author-email jan.jarfalk@unwrongest.com
* @author-website http://www.unwrongest.com
*
* @licence MIT License - http://www.opensource.org/licenses/mit-license.php
*/
(function($){
jQuery.fn.extend({
elastic: function() {
// We will create a div clone of the textarea
// by copying these attributes from the textarea to the div.
var mimics = [
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'fontSize',
'lineHeight',
'fontFamily',
'width',
'fontWeight',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'borderTopStyle',
'borderTopColor',
'borderRightStyle',
'borderRightColor',
'borderBottomStyle',
'borderBottomColor',
'borderLeftStyle',
'borderLeftColor'
];
return this.each( function() {
// Elastic only works on textareas
if ( this.type !== 'textarea' ) {
return false;
}
var $textarea = jQuery(this),
$twin = jQuery('<div />').css({
'position' : 'absolute',
'display' : 'none',
'word-wrap' : 'break-word',
'white-space' :'pre-wrap'
}),
lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'),
minheight = parseInt($textarea.css('height'),10) || lineHeight*3,
maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE,
goalheight = 0;
// Opera returns max-height of -1 if not set
if (maxheight < 0) { maxheight = Number.MAX_VALUE; }
// Append the twin to the DOM
// We are going to meassure the height of this, not the textarea.
$twin.appendTo($textarea.parent());
// Copy the essential styles (mimics) from the textarea to the twin
var i = mimics.length;
while(i--){
$twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString()));
}
// Updates the width of the twin. (solution for textareas with widths in percent)
function setTwinWidth(){
var curatedWidth = Math.floor(parseInt($textarea.width(),10));
if($twin.width() !== curatedWidth){
$twin.css({'width': curatedWidth + 'px'});
// Update height of textarea
update(true);
}
}
// Sets a given height and overflow state on the textarea
function setHeightAndOverflow(height, overflow){
var curratedHeight = Math.floor(parseInt(height,10));
if($textarea.height() !== curratedHeight){
$textarea.css({'height': curratedHeight + 'px','overflow':overflow});
}
}
// This function will update the height of the textarea if necessary
function update(forced) {
// Get curated content from the textarea.
var textareaContent = $textarea.val().replace(/&/g,'&amp;').replace(/ {2}/g, '&nbsp;').replace(/<|>/g, '&gt;').replace(/\n/g, '<br />');
// Compare curated content with curated twin.
var twinContent = $twin.html().replace(/<br>/ig,'<br />');
if(forced || textareaContent+'&nbsp;' !== twinContent){
// Add an extra white space so new rows are added when you are at the end of a row.
$twin.html(textareaContent+'&nbsp;');
// Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height
if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){
var goalheight = $twin.height()+lineHeight;
if(goalheight >= maxheight) {
setHeightAndOverflow(maxheight,'auto');
} else if(goalheight <= minheight) {
setHeightAndOverflow(minheight,'hidden');
} else {
setHeightAndOverflow(goalheight,'hidden');
}
}
}
}
// Hide scrollbars
$textarea.css({'overflow':'hidden'});
// Update textarea size on keyup, change, cut and paste
$textarea.bind('keyup change cut paste', function(){
update();
});
// Update width of twin if browser or textarea is resized (solution for textareas with widths in percent)
$(window).bind('resize', setTwinWidth);
$textarea.bind('resize', setTwinWidth);
$textarea.bind('update', update);
// Compact textarea on blur
$textarea.bind('blur',function(){
if($twin.height() < maxheight){
if($twin.height() > minheight) {
$textarea.height($twin.height());
} else {
$textarea.height(minheight);
}
}
});
// And this line is to catch the browser paste event
$textarea.bind('input paste',function(e){ setTimeout( update, 250); });
// Run update once when elastic is initialized
update();
});
}
});
})(jQuery);

View File

@@ -0,0 +1,77 @@
package service
import org.specs2.mutable.Specification
import java.util.Date
class AccountServiceServiceSpec extends Specification with ServiceSpecBase {
"AccountService" should {
val RootMailAddress = "root@localhost"
"getAllUsers" in { withTestDB{
AccountService.getAllUsers must be like{
case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false)) => ok
}
}}
"getAccountByUserName" in { withTestDB{
AccountService.getAccountByUserName("root") must beSome.like{
case user => user.userName must_== "root"
}
AccountService.getAccountByUserName("invalid user name") must beNone
}}
"getAccountByMailAddress" in { withTestDB{
AccountService.getAccountByMailAddress(RootMailAddress) must beSome
}}
"updateLastLoginDate" in { withTestDB{
val root = "root"
def user() =
AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists"))
user().lastLoginDate must beNone
val date1 = new Date
AccountService.updateLastLoginDate(root)
user().lastLoginDate must beSome.like{ case date =>
date must be_>(date1)
}
val date2 = new Date
Thread.sleep(1000)
AccountService.updateLastLoginDate(root)
user().lastLoginDate must beSome.like{ case date =>
date must be_>(date2)
}
}}
"updateAccount" in { withTestDB{
val root = "root"
def user() =
AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists"))
val newAddress = "new mail address"
AccountService.updateAccount(user().copy(mailAddress = newAddress))
user().mailAddress must_== newAddress
}}
"group" in { withTestDB {
val group1 = "group1"
val user1 = "root"
AccountService.createGroup(group1, None)
AccountService.getGroupMembers(group1) must_== Nil
AccountService.getGroupsByUserName(user1) must_== Nil
AccountService.updateGroupMembers(group1, List(user1))
AccountService.getGroupMembers(group1) must_== List(user1)
AccountService.getGroupsByUserName(user1) must_== List(group1)
AccountService.updateGroupMembers(group1, Nil)
AccountService.getGroupMembers(group1) must_== Nil
AccountService.getGroupsByUserName(user1) must_== Nil
}}
}
}

View File

@@ -0,0 +1,26 @@
package service
import scala.slick.session.Database
import util.ControlUtil._
import java.sql.DriverManager
import org.apache.commons.io.FileUtils
import scala.util.Random
import java.io.File
trait ServiceSpecBase {
def withTestDB[A](action: => A): A = {
util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){
dir =>
val (url, user, pass) = (s"jdbc:h2:${dir}", "sa", "sa")
org.h2.Driver.load()
using(DriverManager.getConnection(url, user, pass)){ conn =>
servlet.AutoUpdate.versions.reverse.foreach(_.update(conn))
}
Database.forURL(url, user, pass).withSession {
action
}
}
}
}

View File

@@ -0,0 +1,38 @@
package util
import org.specs2.mutable._
class StringUtilSpec extends Specification {
"urlDecode" should {
"decode encoded string to original string" in {
val encoded = StringUtil.urlEncode("あいうえお")
StringUtil.urlDecode(encoded) mustEqual "あいうえお"
}
}
"splitWords" should {
"split string by whitespaces" in {
val split = StringUtil.splitWords("aa bb\tcc dd \t ee")
split mustEqual Array("aa", "bb", "cc", "dd", "ee")
}
}
"escapeHtml" should {
"escape &, <, > and \"" in {
StringUtil.escapeHtml("<a href=\"/test\">a & b</a>") mustEqual "&lt;a href=&quot;/test&quot;&gt;a &amp; b&lt;/a&gt;"
}
}
"md5" should {
"generate MD5 hash" in {
StringUtil.md5("abc") mustEqual "900150983cd24fb0d6963f7d28e17f72"
}
}
"sha1" should {
"generate SHA1 hash" in {
StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d"
}
}
}

View File

@@ -0,0 +1,34 @@
package util
import org.specs2.mutable._
class ValidationsSpec extends Specification with Validations {
"identifier" should {
"validate id string " in {
identifier.validate("id", "aa_ZZ-00.01") mustEqual None
identifier.validate("id", "_aaaa") mustEqual Some("id starts with invalid character.")
identifier.validate("id", "-aaaa") mustEqual Some("id starts with invalid character.")
identifier.validate("id", "aa_ZZ#01") mustEqual Some("id contains invalid character.")
}
}
"color" should {
"validate color string " in {
color.validate("color", "#88aaff") mustEqual None
color.validate("color", "#gghhii") mustEqual Some("color must be '#[0-9a-fA-F]{6}'.")
}
}
"date" should {
// "validate date string " in {
// date().validate("date", "2013-10-05", Map[String, String]()) mustEqual Nil
// date().validate("date", "2013-10-5" , Map[String, String]()) mustEqual List(("date", "date must be '\\d{4}-\\d{2}-\\d{2}'."))
// }
"convert date string " in {
val result = date().convert("2013-10-05")
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result) mustEqual "2013-10-05 00:00:00"
}
}
}

View File

@@ -0,0 +1,103 @@
package view
import java.util.Date
import org.specs2.mutable._
import service.RequestCache
import model.Account
import service.SystemSettingsService.SystemSettings
import twirl.api.Html
class AvatarImageProviderSpec extends Specification {
implicit val context = app.Context("", None, "", null)
"getAvatarImageHtml" should {
"show Gravatar image for no image account if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(true))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"http://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show uploaded image even if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(Some("icon.png"))), createSystemSettings(true))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/user/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show local image for no image account if gravatar integration is disabled" in {
val provider = new AvatarImageProviderImpl(Some(createAccount(None)), createSystemSettings(false))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/user/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show Gravatar image for specified mail address if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(true))
provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual
"<img src=\"http://www.gravatar.com/avatar/4712f9b0e63f56ad952ad387eaa23b9c?s=20\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show unknown image for unknown user if gravatar integration is enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(true))
provider.toHtml("user", 20).toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"show unknown image for specified mail address if gravatar integration is disabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(false))
provider.toHtml("user", 20, "hoge@hoge.com").toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" />"
}
"add tooltip if it's enabled" in {
val provider = new AvatarImageProviderImpl(None, createSystemSettings(false))
provider.toHtml("user", 20, "hoge@hoge.com", true).toString mustEqual
"<img src=\"/_unknown/_avatar\" class=\"avatar\" style=\"width: 20px; height: 20px;\" data-toggle=\"tooltip\" title=\"user\"/>"
}
}
private def createAccount(image: Option[String]) =
Account(
userName = "user",
fullName = "user@localhost",
mailAddress = "",
password = "",
isAdmin = false,
url = None,
registeredDate = new Date(),
updatedDate = new Date(),
lastLoginDate = None,
image = image,
isGroupAccount = false)
private def createSystemSettings(useGravatar: Boolean) =
SystemSettings(
allowAccountRegistration = false,
gravatar = useGravatar,
notification = false,
smtp = None,
ldapAuthentication = false,
ldap = None)
/**
* Adapter to test AvatarImageProviderImpl.
*/
class AvatarImageProviderImpl(account: Option[Account], settings: SystemSettings)
extends AvatarImageProvider with RequestCache {
def toHtml(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)
(implicit context: app.Context): Html = getAvatarImageHtml(userName, size, mailAddress, tooltip)
override def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = account
override def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = account
override def getSystemSettings()(implicit context: app.Context): SystemSettings = settings
}
}

View File

@@ -0,0 +1,69 @@
package view
import org.specs2.mutable._
import util.ControlUtil._
class PaginationSpec extends Specification {
"max" should {
"return max page number" in {
val pagination = Pagination(1, 100, 10, 6)
pagination.max mustEqual 10
}
}
"omitLeft and omitRight" should {
"return true if pagination links at their side will be omitted" in {
defining(Pagination(1, 100, 10, 6)){ pagination =>
pagination.omitLeft mustEqual false
pagination.omitRight mustEqual true
}
defining(Pagination(9, 100, 10, 6)){ pagination =>
pagination.omitLeft mustEqual true
pagination.omitRight mustEqual false
}
}
}
"visibleFor" should {
"return true for visible pagination links" in {
defining(Pagination(1, 100, 10, 6)){ pagination =>
pagination.visibleFor(1) mustEqual true
pagination.visibleFor(2) mustEqual true
pagination.visibleFor(3) mustEqual true
pagination.visibleFor(4) mustEqual true
pagination.visibleFor(5) mustEqual true
pagination.visibleFor(6) mustEqual false
pagination.visibleFor(7) mustEqual false
pagination.visibleFor(8) mustEqual false
pagination.visibleFor(9) mustEqual false
pagination.visibleFor(10) mustEqual true
}
defining(Pagination(5, 100, 10, 6)){ pagination =>
pagination.visibleFor(1) mustEqual true
pagination.visibleFor(2) mustEqual false
pagination.visibleFor(3) mustEqual false
pagination.visibleFor(4) mustEqual true
pagination.visibleFor(5) mustEqual true
pagination.visibleFor(6) mustEqual true
pagination.visibleFor(7) mustEqual false
pagination.visibleFor(8) mustEqual false
pagination.visibleFor(9) mustEqual false
pagination.visibleFor(10) mustEqual true
}
defining(Pagination(8, 100, 10, 6)){ pagination =>
pagination.visibleFor(1) mustEqual true
pagination.visibleFor(2) mustEqual false
pagination.visibleFor(3) mustEqual false
pagination.visibleFor(4) mustEqual false
pagination.visibleFor(5) mustEqual false
pagination.visibleFor(6) mustEqual true
pagination.visibleFor(7) mustEqual true
pagination.visibleFor(8) mustEqual true
pagination.visibleFor(9) mustEqual true
pagination.visibleFor(10) mustEqual true
}
}
}
}