Merge branch 'master' into #33_match-by-email

Conflicts:
	src/main/scala/view/helpers.scala
This commit is contained in:
takezoe
2013-10-16 01:51:44 +09:00
180 changed files with 8151 additions and 2272 deletions

View File

@@ -6,20 +6,25 @@ GitBucket is the easily installable Github clone written with Scala.
The current version of GitBucket provides a basic features below: The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only) - Public / Private Git repository (http access only)
- Repository viewer (some advanced features are not implemented) - Repository viewer (some advanced features such as online file editing are not implemented)
- Repository search (Code and Issues)
- Wiki - Wiki
- Issues - Issues
- Fork / Pull request
- Mail notification
- Activity timeline - Activity timeline
- User management (for Administrators) - User management (for Administrators)
- Group (like Organization in Github)
- LDAP integration
- Gravatar support
Following features are not implemented, but we will make them in the future release! Following features are not implemented, but we will make them in the future release!
- Fork and pull request - File editing in repository viewer
- Search - Comment for the changeset
- Network graph - Network graph
- Statics - Statics
- Watch / Star - Watch / Star
- Team management (like Organization in Github)
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki). If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -32,29 +37,64 @@ Installation
The default administrator account is **root** and password is **root**. The default administrator account is **root** and password is **root**.
To upgrade GitBucket, only replace gitbucket.war. (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.
- --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 Release Notes
-------- --------
### 1.3 - xx Jul 2013 ### 1.6 - 1 Oct 2013
- Batch updating for issues. - Web hook
- Display assigned user on issue list. - Performance improvement for pull request
- User icon and Gravatar support. - Executable war file
- Convert @xxxx to link to the account page. - Specify suitable Content-Type for downloaded files in the repository viewer
- Add copy to clipboard button for git clone URL. - Fix some bugs
- Allows multi-byte characters as wiki page name.
- Allows to create the empty repository. ### 1.5 - 4 Sep 2013
- 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
- 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
- Allow multi-byte characters as wiki page name
- Allow to create the empty repository
- Fix some bugs
### 1.2 - 09 Jul 2013 ### 1.2 - 09 Jul 2013
- Added activity timeline. - Add activity timeline
- Bugfix for Git 1.8.1.5 or later. - Bugfix for Git 1.8.1.5 or later
- Allows multi-byte characters as label. - Allow multi-byte characters as label
- Fixed some bugs. - Fix some bugs
### 1.1 - 05 Jul 2013 ### 1.1 - 05 Jul 2013
- Fixed some bugs. - Fix some bugs
- Upgrade to JGit 3.0. - Upgrade to JGit 3.0
### 1.0 - 04 Jul 2013 ### 1.0 - 04 Jul 2013
- This is a first public release. - This is a first public release

61
build.xml Normal file
View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir=".">
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.10"/>
<property name="gitbucket.version" value="0.0.1"/>
<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 executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
<arg line="clean compile test package" />
</exec>
</target>
<target name="embed" depends="war">
<mkdir dir="${embed.classes.dir}"/>
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${embed.classes.dir}"
update = "true"
includes="javax/**,org/**"/>
<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"/>
</target>
<target name="rename" depends="embed">
<rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="all" depends="rename">
</target>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,8 +23,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>37</x> <x>33</x>
<y>36</y> <y>18</y>
</constraint> </constraint>
<sourceConnections/> <sourceConnections/>
<targetConnections> <targetConnections>
@@ -51,8 +51,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>751</x> <x>723</x>
<y>47</y> <y>138</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel> <net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -79,8 +79,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>882</x> <x>1182</x>
<y>239</y> <y>339</y>
</constraint> </constraint>
<sourceConnections/> <sourceConnections/>
<targetConnections> <targetConnections>
@@ -108,8 +108,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>940</x> <x>1301</x>
<y>615</y> <y>836</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -138,8 +138,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>420</x> <x>684</x>
<y>758</y> <y>858</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -167,8 +167,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>307</x> <x>293</x>
<y>356</y> <y>478</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel> <net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -210,8 +210,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>641</x> <x>875</x>
<y>569</y> <y>677</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel> <net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -283,9 +283,14 @@
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>MILESTONE_NAME</columnName> <columnName>TITLE</columnName>
<logicalName>Milestone Name</logicalName> <logicalName>Title</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType">
<name>VARCHAR</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>12</type>
</columnType>
<size>100</size> <size>100</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -293,6 +298,49 @@
<autoIncrement>false</autoIncrement> <autoIncrement>false</autoIncrement>
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>DESCRIPTION</columnName>
<logicalName>Description</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>TEXT</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>2005</type>
</columnType>
<size></size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>DUE_DATE</columnName>
<logicalName>Due Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>TIMESTAMP</name>
<logicalName>日時</logicalName>
<supportSize>false</supportSize>
<type>93</type>
</columnType>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>CLOSED_DATE</columnName>
<logicalName>Closed Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns> </columns>
<indices/> <indices/>
<backgroundColor> <backgroundColor>
@@ -350,6 +398,36 @@
</entry> </entry>
</references> </references>
</net.java.amateras.db.visual.model.ForeignKeyModel> </net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../../../../../../../.."/>
<foreignKeyName>ISSUE_FK_2</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ASSIGNED_USER_NAME</columnName>
<logicalName>Assinged User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections> </sourceConnections>
<targetConnections> <targetConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel> <net.java.amateras.db.visual.model.ForeignKeyModel>
@@ -375,8 +453,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>26</x> <x>18</x>
<y>660</y> <y>776</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -462,6 +540,22 @@
<autoIncrement>true</autoIncrement> <autoIncrement>true</autoIncrement>
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTION</columnName>
<logicalName>Action</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>VARCHAR</name>
<logicalName>文字列</logicalName>
<supportSize>true</supportSize>
<type>12</type>
</columnType>
<size>20</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description>Expand to VARCHAR(20) from VARCHAR(10) in 1.3</description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/> <net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>CONTENT</columnName> <columnName>CONTENT</columnName>
@@ -498,7 +592,7 @@
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>UPDATED_DATE</columnName> <columnName>UPDATED_DATE</columnName>
<logicalName>Updated Date</logicalName> <logicalName>Updated Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size> <size>10</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -572,10 +666,11 @@
<autoIncrement>false</autoIncrement> <autoIncrement>false</autoIncrement>
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>TITLE</columnName> <columnName>TITLE</columnName>
<logicalName>Title</logicalName> <logicalName>Title</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<size></size> <size></size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -586,7 +681,7 @@
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>CONTENT</columnName> <columnName>CONTENT</columnName>
<logicalName>Content</logicalName> <logicalName>Content</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/>
<size></size> <size></size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -597,7 +692,7 @@
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>REGISTERED_DATE</columnName> <columnName>REGISTERED_DATE</columnName>
<logicalName>Registered Date</logicalName> <logicalName>Registered Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size> <size>10</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -608,7 +703,7 @@
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>UPDATED_DATE</columnName> <columnName>UPDATED_DATE</columnName>
<logicalName>Updated Date</logicalName> <logicalName>Updated Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[7]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[8]/columnType"/>
<size>10</size> <size>10</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
@@ -801,8 +896,8 @@
<constraint> <constraint>
<height>-1</height> <height>-1</height>
<width>-1</width> <width>-1</width>
<x>388</x> <x>481</x>
<y>166</y> <y>361</y>
</constraint> </constraint>
<sourceConnections> <sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
@@ -862,6 +957,250 @@
</net.java.amateras.db.visual.model.ForeignKeyModel> </net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel"/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>1199</x>
<y>25</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../../../../../../../.."/>
<foreignKeyName>ACTIVITY_FK_2</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_USER_NAME</columnName>
<logicalName>Activity User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>ACTIVITY</tableName>
<logicalName>Activity</logicalName>
<description>Since 1.2</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_ID</columnName>
<logicalName>Activity ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name>
<logicalName>整数</logicalName>
<supportSize>false</supportSize>
<type>4</type>
</columnType>
<size>10</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>true</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel reference="../../sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_TYPE</columnName>
<logicalName>Activity Type</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>MESSAGE</columnName>
<logicalName>Message</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size></size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ADDITIONAL_INFO</columnName>
<logicalName>Additional Information</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size></size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ACTIVITY_DATE</columnName>
<logicalName>Activity Date</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[6]/columnType"/>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>ACTIVITY_FK_1</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>1451</x>
<y>577</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>COMMIT_LOG</tableName>
<logicalName>Commit Log</logicalName>
<description>Since 1.2</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>COMMIT_ID</columnName>
<logicalName>Commit ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>40</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>COMMIT_LOG_FK_1</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</targetConnections> </targetConnections>
<error></error> <error></error>
<linkedPath></linkedPath> <linkedPath></linkedPath>
@@ -1062,6 +1401,100 @@
</net.java.amateras.db.visual.model.ForeignKeyModel> </net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/> <net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[4]"/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel">
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>432</x>
<y>240</y>
</constraint>
<sourceConnections>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/>
<net.java.amateras.db.visual.model.ForeignKeyModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.ForeignKeyModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../../../../.."/>
<foreignKeyName>GROUP_MEMBER_FK_2</foreignKeyName>
<references/>
</net.java.amateras.db.visual.model.ForeignKeyModel>
</sourceConnections>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>GROUP_MEMBER</tableName>
<logicalName>Group Member</logicalName>
<description>Since 1.4</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>GROUP_NAME</columnName>
<logicalName>Group Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>100</size>
<notNull>true</notNull>
<primaryKey>true</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>255</red>
<green>255</green>
<blue>206</blue>
</backgroundColor>
<sql></sql>
</source>
<target class="net.java.amateras.db.visual.model.TableModel" reference="../../.."/>
<foreignKeyName>GROUP_MEMBER_FK_1</foreignKeyName>
<references>
<entry>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../../net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/references/entry/net.java.amateras.db.visual.model.ColumnModel"/>
<net.java.amateras.db.visual.model.ColumnModel reference="../../../source/columns/net.java.amateras.db.visual.model.ColumnModel"/>
</entry>
</references>
</net.java.amateras.db.visual.model.ForeignKeyModel>
<net.java.amateras.db.visual.model.ForeignKeyModel reference="../net.java.amateras.db.visual.model.ForeignKeyModel[6]/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]"/>
</targetConnections> </targetConnections>
<error></error> <error></error>
<linkedPath></linkedPath> <linkedPath></linkedPath>
@@ -1089,8 +1522,8 @@
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>PASSWORD</columnName> <columnName>PASSWORD</columnName>
<logicalName>Password</logicalName> <logicalName>Password</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[2]/columnType"/> <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/columns/net.java.amateras.db.visual.model.ColumnModel[4]/columnType"/>
<size>20</size> <size>40</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
<description></description> <description></description>
@@ -1098,18 +1531,18 @@
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel> <net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_TYPE</columnName> <columnName>ADMINISTRATOR</columnName>
<logicalName>User Type</logicalName> <logicalName>Administrator</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType"> <columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name> <name>BOOLEAN</name>
<logicalName>整数</logicalName> <logicalName>真偽値</logicalName>
<supportSize>false</supportSize> <supportSize>false</supportSize>
<type>4</type> <type>16</type>
</columnType> </columnType>
<size>10</size> <size>10</size>
<notNull>true</notNull> <notNull>true</notNull>
<primaryKey>false</primaryKey> <primaryKey>false</primaryKey>
<description>0:Normal 1:Administrator</description> <description></description>
<autoIncrement>false</autoIncrement> <autoIncrement>false</autoIncrement>
<defaultValue>0</defaultValue> <defaultValue>0</defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
@@ -1157,6 +1590,33 @@
<autoIncrement>false</autoIncrement> <autoIncrement>false</autoIncrement>
<defaultValue></defaultValue> <defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel> </net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>IMAGE</columnName>
<logicalName>Image</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description>Since 1.3</description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>GROUP_ACCOUNT</columnName>
<logicalName>Group Account</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>BOOLEAN</name>
<logicalName>真偽値</logicalName>
<supportSize>false</supportSize>
<type>16</type>
</columnType>
<size>10</size>
<notNull>true</notNull>
<primaryKey>false</primaryKey>
<description>Since 1.4</description>
<autoIncrement>false</autoIncrement>
<defaultValue>FALSE</defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns> </columns>
<indices> <indices>
<net.java.amateras.db.visual.model.IndexModel> <net.java.amateras.db.visual.model.IndexModel>
@@ -1184,6 +1644,91 @@
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/> <net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/> <net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/> <net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[3]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[7]/source"/>
<net.java.amateras.db.visual.model.TableModel reference="../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[6]/source"/>
<net.java.amateras.db.visual.model.TableModel>
<listeners serialization="custom">
<java.beans.PropertyChangeSupport>
<default>
<source class="net.java.amateras.db.visual.model.TableModel" reference="../../../.."/>
<propertyChangeSupportSerializedDataVersion>2</propertyChangeSupportSerializedDataVersion>
</default>
<null/>
</java.beans.PropertyChangeSupport>
</listeners>
<constraint>
<height>-1</height>
<width>-1</width>
<x>410</x>
<y>860</y>
</constraint>
<sourceConnections/>
<targetConnections/>
<error></error>
<linkedPath></linkedPath>
<tableName>ISSUE_OUTLINE_VIEW</tableName>
<logicalName>Issue Outline View</logicalName>
<description>Since 1.4</description>
<columns>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>USER_NAME</columnName>
<logicalName>User Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>REPOSITORY_NAME</columnName>
<logicalName>Repository Name</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../../../net.java.amateras.db.visual.model.TableModel/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/source/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/sourceConnections/net.java.amateras.db.visual.model.ForeignKeyModel[2]/target/targetConnections/net.java.amateras.db.visual.model.ForeignKeyModel/source/columns/net.java.amateras.db.visual.model.ColumnModel[5]/columnType"/>
<size>100</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>ISSUE_ID</columnName>
<logicalName>Issue ID</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType">
<name>INT</name>
<logicalName>整数</logicalName>
<supportSize>false</supportSize>
<type>4</type>
</columnType>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
<net.java.amateras.db.visual.model.ColumnModel>
<columnName>COMMENT_COUNT</columnName>
<logicalName>Comment Count</logicalName>
<columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[3]/columnType"/>
<size>10</size>
<notNull>false</notNull>
<primaryKey>false</primaryKey>
<description></description>
<autoIncrement>false</autoIncrement>
<defaultValue></defaultValue>
</net.java.amateras.db.visual.model.ColumnModel>
</columns>
<indices/>
<backgroundColor>
<red>210</red>
<green>232</green>
<blue>249</blue>
</backgroundColor>
<sql></sql>
</net.java.amateras.db.visual.model.TableModel>
</children> </children>
<dommains/> <dommains/>
<dialectName>H2</dialectName> <dialectName>H2</dialectName>

751
etc/icons.svg Normal file
View File

@@ -0,0 +1,751 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819"
height="1052.3622047"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="icons.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.4"
inkscape:cx="629.30023"
inkscape:cy="281.44758"
inkscape:document-units="px"
inkscape:current-layer="layer1-9"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:snap-global="true"
inkscape:snap-grids="false"
inkscape:snap-page="false"
inkscape:snap-bbox="true"
inkscape:bbox-paths="false"
inkscape:bbox-nodes="false"
inkscape:snap-to-guides="true">
<inkscape:grid
type="xygrid"
id="grid3080" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="layer1-9"
inkscape:label="Layer 1"
transform="matrix(0.66004549,0,0,0.66004549,12.445368,29.409765)">
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.51504707px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 865.73247,686.51304 c 0,0 19.28074,14.1795 55.09542,13.7739 35.81468,-0.4056 45.91286,-13.7739 45.91286,-13.7739 l 31.84606,-118.8515 -163.46293,0 z"
id="path4000"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcccc" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:25.84518814;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 306.9072,1201.5096 c 0,0 3.44333,-28.5633 47.63498,-35.4849 15.10377,-2.3655 48.7968,-8.2798 48.7968,-42.5816"
id="path3207"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-6.1348784"
sodipodi:nodetypes="csc"
inkscape:transform-center-y="1.9434039e-005" />
<path
style="fill:none;stroke:#b3b3b3;stroke-width:26.60422707;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 76.384718,1086.1545 c 0,82.8617 105.181182,77.9295 105.181182,77.9295"
id="path4318"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.18291342;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3935"
width="266.2222"
height="35.127476"
x="-4.6761055"
y="865.6405" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.34059906;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 664.11762,675.10023 -74.94096,87.54344 20.17642,-92.15099 z"
id="path3894-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088-5-5"
width="169.03172"
height="105.81662"
x="547.64557"
y="573.36456" />
<path
inkscape:connector-curvature="0"
id="path3850"
d="m 445.03908,191.42833 0,-128.577242 c 0,0 1.85983,-15.30681 -16.73849,-15.30681 -18.59831,0 -51.14538,0 -51.14538,0"
style="fill:none;stroke:#008000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991"
transform="translate(-137.57539,-163.64471)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#008000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-94.824045,-115.22257)" />
<rect
id="rect2995"
y="54.447956"
x="104.3765"
height="99.221695"
width="29.189819"
style="fill:#008000;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997"
y="173.24185"
x="104.63474"
height="26.258072"
width="29.724136"
style="fill:#008000;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="68.361099"
x="330.18893"
height="104.27071"
width="3.2554624"
id="rect3818"
style="fill:#ffffff;stroke:#008000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-20.394061,56.890898)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-21.929587,-93.432709)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,92.394578,56.992418)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0"
style="fill:#ffffff;stroke:#008000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852"
d="m 404.75446,10.803052 0,70.691447 L 359.1655,49.35988 z"
style="fill:#008000;stroke:#008000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3850-4"
d="m 448.69288,446.18012 0,-128.57725 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
style="fill:none;stroke:#800000;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991-8"
transform="translate(-133.92158,91.107081)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#800000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-8"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-91.170233,139.52922)" />
<rect
id="rect2995-2"
y="309.19974"
x="108.03028"
height="99.221687"
width="29.189819"
style="fill:#800000;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-4"
y="427.99362"
x="108.28852"
height="26.258072"
width="29.724136"
style="fill:#800000;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="323.11288"
x="333.84274"
height="104.27072"
width="3.2554622"
id="rect3818-5"
style="fill:#ffffff;stroke:#800000;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-16.740254,311.64269)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-5"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-18.275774,161.31908)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-1"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,96.048392,311.7442)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-7"
style="fill:#ffffff;stroke:#800000;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-1"
d="m 408.40826,265.55484 0,70.69144 -45.58895,-32.13461 z"
style="fill:#800000;stroke:#800000;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#cccccc"
id="rect2985"
width="308.26331"
height="308.26331"
x="647.59973"
y="19.593252" />
<path
sodipodi:type="arc"
style="fill:#ffffff"
id="path2989"
sodipodi:cx="246.42857"
sodipodi:cy="327.36218"
sodipodi:rx="35"
sodipodi:ry="35"
d="m 281.42857,327.36218 c 0,19.32997 -15.67003,35 -35,35 -19.32996,0 -35,-15.67003 -35,-35 0,-19.32996 15.67004,-35 35,-35 19.32997,0 35,15.67004 35,35 z"
transform="matrix(2.9255147,0,0,2.9255147,83.281176,-813.70029)" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:1.59620917px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 715.46559,327.54005 179.96463,0 -89.85466,-201.67002 z"
id="path2993-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path3850-1"
d="m 447.16245,696.53224 0,-128.57724 c 0,0 1.85984,-15.30681 -16.73848,-15.30681 -18.59831,0 -51.14539,0 -51.14539,0"
style="fill:none;stroke:#b3b3b3;stroke-width:22.7257061;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
id="path2991-7"
transform="translate(-135.45201,341.45921)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,-92.700665,389.88135)" />
<rect
id="rect2995-0"
y="559.55188"
x="106.49989"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9"
y="678.34576"
x="106.75813"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="573.46503"
x="332.31235"
height="104.27072"
width="3.2554622"
id="rect3818-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-18.270676,561.99481)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-19.806206,411.67121)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,94.517962,562.09633)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-0-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
inkscape:connector-curvature="0"
id="path3852-4"
d="m 406.87783,515.90696 0,70.69145 -45.58895,-32.13462 z"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.83335358px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<rect
style="fill:#ffffff;stroke:#ffffff;stroke-width:32.11899948;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088"
width="188.24272"
height="117.84301"
x="578.56567"
y="534.50873" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:11.66586208;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 667.1767,647.76042 746.75901,734.99486 725.333,643.16913 z"
id="path3894"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:28.84111404;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3088-5"
width="169.03172"
height="105.81661"
x="595.6264"
y="533.38885" />
<path
id="path2991-7-7"
transform="matrix(0.81013086,0,0,0.81013086,-79.003905,648.21364)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#b3b3b3;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-1"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.56153831,0,0,0.56153831,-15.312437,720.57846)" />
<path
id="path2991-7-1"
transform="translate(167.79377,599.09604)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-5"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,210.54515,647.51817)" />
<rect
id="rect2995-0-2"
y="817.18872"
x="409.74567"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9-7"
y="935.98169"
x="410.00391"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 383.15829,850.33665 -64.6851,-36.2114 10.70013,55.95688 53.98497,-19.74548 z"
id="rect4046-3"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 372.50197,843.46474 -43.65605,-24.43447 6.99871,38.15621 36.65734,-13.72174 z"
id="rect4046"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 462.88559,934.94792 64.6851,36.21128 -10.70013,-55.95672 -53.98497,19.74544 z"
id="rect4046-3-2"
inkscape:connector-curvature="0" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.98877633;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
d="m 471.91864,943.98419 43.65605,24.43442 -6.99871,-38.15607 -36.65734,13.72165 z"
id="rect4046-1"
inkscape:connector-curvature="0" />
<path
id="path2991-7-79"
transform="translate(439.9024,596.03518)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-54"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.83611704,0,0,0.83611704,482.65378,644.45731)" />
<rect
style="fill:#ffffff;stroke:#ffffff;stroke-width:7.27556181;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4271"
width="55.131588"
height="89.475853"
x="1123.0723"
y="8.2489862"
transform="matrix(0.69198127,0.72191545,-0.69198127,0.72191545,0,0)" />
<rect
id="rect2995-0-3-3"
y="1106.4344"
x="-89.869194"
height="57.711208"
width="24.529409"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.77681416"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
<rect
id="rect2995-0-3-2"
y="7.221128"
x="1139.5251"
height="82.866272"
width="24.378254"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:0.92796957"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
id="rect2995-0-3"
y="814.12781"
x="681.85431"
height="99.221687"
width="29.189819"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:1.11112404" />
<rect
id="rect2997-9-1"
y="932.58148"
x="682.54327"
height="26.258072"
width="29.724136"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:0.57680577" />
<rect
y="1088.6628"
x="76.264809"
height="104.27072"
width="3.2554622"
id="rect3818-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-274.3181,1077.1951)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-275.85363,926.87175)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-161.78913,1021.9512)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-7"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<rect
y="1087.278"
x="304.77451"
height="104.27072"
width="3.2554622"
id="rect3818-4-8-4"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:22.7257061;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-45.808546,1075.8101)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-4-8-7-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,-47.344075,925.48675)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
transform="matrix(1.0049237,0,0,0.61497516,53.509086,972.5163)"
d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915"
sodipodi:rx="21.718279"
sodipodi:cy="230.89374"
sodipodi:cx="351.02802"
id="path3795-8-4-8-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:12.04511166;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
<path
sodipodi:type="arc"
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:6.68107271;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="path3992-4"
sodipodi:cx="490.42908"
sodipodi:cy="950.84186"
sodipodi:rx="18.487062"
sodipodi:ry="26.506598"
d="m 508.91614,950.84186 c 0,14.63919 -8.27694,26.5066 -18.48706,26.5066 -10.21013,0 -18.48707,-11.86741 -18.48707,-26.5066 0,-14.63919 8.27694,-26.5066 18.48707,-26.5066 10.21012,0 18.48706,11.86741 18.48706,26.5066 z"
transform="matrix(4.8923198,0,0,1.0737805,-1482.0573,-466.94845)" />
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
d="m 967.57233,525.26244 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19027,0 0,-27.1288 29.354,0 0,-41.2377 -29.354,0 0,-30.6797 -41.19027,0 z"
id="rect2995-0-2-7"
inkscape:connector-curvature="0" />
<path
id="path2991-7-2"
transform="translate(717.27126,597.74227)"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#b3b3b3;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc" />
<path
id="path2993-4-7"
d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571"
sodipodi:rx="104.28571"
sodipodi:cy="290.93362"
sodipodi:cx="255.71428"
style="fill:#ffffff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
sodipodi:type="arc"
transform="matrix(0.7638244,0,0,0.7638244,777.85958,666.54744)" />
<rect
id="rect2995-0-6"
y="-220.76018"
x="1298.3352"
height="189.71017"
width="28.775486"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:1.52545774"
transform="matrix(0.67068946,0.74173826,-0.74173826,0.67068946,0,0)" />
<g
id="g4284"
transform="translate(-77.916708,-8.657412)">
<path
sodipodi:nodetypes="czcczcc"
inkscape:connector-curvature="0"
id="rect4201"
d="m 568.37427,1080.8464 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43273,8.6574 40.43273,8.6574 l 0,141.4674 c 0,0 -20.97035,-7.7215 -40.43273,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538029;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4" />
<rect
y="1108.1473"
x="597.4068"
height="5.4857273"
width="55.265846"
id="rect4203"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
y="1142.7776"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
y="1176.1093"
x="598.48895"
height="5.4857273"
width="55.26585"
id="rect4203-2-3"
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<path
sodipodi:nodetypes="czc"
inkscape:connector-curvature="0"
id="path4245"
d="m 563.55369,1233.6274 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29117,14.7566 46.29117,14.7566"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<g
transform="matrix(-1.0032405,0,0,1,1329.8708,99.560238)"
id="g4277">
<path
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:14.36538124;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4"
d="m 519.67634,980.83663 c 0,0 55.60005,-9.5933 75.06243,-8.6574 19.46238,0.9359 40.43272,8.6574 40.43272,8.6574 l 0,141.46737 c 0,0 -20.97034,-7.7215 -40.43272,-8.6574 -19.46238,-0.9359 -75.06243,8.6574 -75.06243,8.6574 z"
id="rect4201-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czcczcc" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-21"
width="55.26585"
height="5.4857273"
x="548.70886"
y="1008.1376" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-2-6"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1042.7678" />
<rect
style="fill:#ffffff;stroke:#b3b3b3;stroke-width:11.82844734;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4203-2-3-8"
width="55.26585"
height="5.4857273"
x="549.79102"
y="1076.0995" />
<path
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.6372261;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="m 514.85576,1133.6176 c 0,0 59.11965,-16.1473 81.00954,-14.7566 21.8899,1.3907 46.29116,14.7566 46.29116,14.7566"
id="path4245-5"
inkscape:connector-curvature="0"
sodipodi:nodetypes="czc" />
</g>
</g>
<g
id="g3107"
transform="matrix(0.53086704,-0.53086704,0.53086704,0.53086704,-205.0028,934.47839)">
<rect
y="1165.7029"
x="793.91357"
height="177.36816"
width="131.91675"
id="rect3075"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:19.58793259;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<rect
transform="matrix(0.69911762,0.71500668,-0.71500668,0.69911762,0,0)"
y="145.59781"
x="1379.6274"
height="95.711494"
width="95.711456"
id="rect3075-1"
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:12.25645447;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" />
<path
transform="matrix(1.5150471,0,0,1.5150471,-201.2129,-64.133761)"
d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
sodipodi:ry="10"
sodipodi:rx="10"
sodipodi:cy="812.36218"
sodipodi:cx="700"
id="path3100"
style="fill:#ffffff;stroke:#ffffff;stroke-width:12.69999981;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
sodipodi:type="arc" />
</g>
<path
style="fill:#b3b3b3;stroke:#ffffff;stroke-width:12.98546886;stroke-miterlimit:4;stroke-dasharray:none"
d="m 937.41093,1044.4944 0,30.6797 -28.50183,0 0,41.2377 28.50183,0 0,27.1288 41.19033,0 0,-27.1288 29.35404,0 0,-41.2377 -29.35404,0 0,-30.6797 -41.19033,0 z"
id="rect2995-0-2-7-7"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

1
project/build.properties Normal file
View File

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

View File

@@ -2,6 +2,7 @@ import sbt._
import Keys._ import Keys._
import org.scalatra.sbt._ import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._ import org.scalatra.sbt.PluginKeys._
import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._ import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -9,8 +10,8 @@ object MyBuild extends Build {
val Organization = "jp.sf.amateras" val Organization = "jp.sf.amateras"
val Name = "gitbucket" val Name = "gitbucket"
val Version = "0.0.1" val Version = "0.0.1"
val ScalaVersion = "2.10.1" val ScalaVersion = "2.10.3"
val ScalatraVersion = "2.2.0" val ScalatraVersion = "2.2.1"
lazy val project = Project ( lazy val project = Project (
"gitbucket", "gitbucket",
@@ -20,24 +21,35 @@ object MyBuild extends Build {
name := Name, name := Name,
version := Version, version := Version,
scalaVersion := ScalaVersion, scalaVersion := ScalaVersion,
resolvers += Classpaths.typesafeReleases, resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
),
scalacOptions := Seq("-deprecation"),
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.apache.commons" % "commons-io" % "1.3.2",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "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", "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-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"com.typesafe.slick" %% "slick" % "1.0.1", "com.typesafe.slick" %% "slick" % "1.0.1",
"com.h2database" % "h2" % "1.3.171", "com.novell.ldap" % "jldap" % "2009-10-07",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", "com.h2database" % "h2" % "1.3.173",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")) "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")),
"junit" % "junit" % "4.11" % "test"
), ),
EclipseKeys.withSource := true 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: _*) ) ++ seq(Twirl.settings: _*)
) )
} }

View File

@@ -1,7 +1,9 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2") addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0") addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.2.0") addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1") addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")

1
sbt.sh Executable file
View File

@@ -0,0 +1 @@
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"

View File

@@ -0,0 +1,53 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
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 = "/";
for(String arg: args){
if(arg.startsWith("--") && arg.contains("=")){
String[] dim = arg.split("=");
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")){
contextPath = dim[1];
}
}
}
}
Server server = new Server();
SelectChannelConnector connector = new SelectChannelConnector();
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);
context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
context.setServer(server);
context.setWar(location.toExternalForm());
server.setHandler(context);
server.start();
server.join();
}
}

View File

@@ -1,4 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<logger name="scala.slick" level="INFO" /> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!--
<logger name="service.WebHookService" level="DEBUG" />
<logger name="servlet" level="DEBUG" />
-->
</configuration> </configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,24 @@
CREATE TABLE GROUP_MEMBER(
GROUP_NAME VARCHAR(100) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL
);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);

View File

@@ -0,0 +1,21 @@
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
CREATE TABLE PULL_REQUEST(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
ISSUE_ID INT NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
REQUEST_USER_NAME VARCHAR(100) NOT NULL,
REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
REQUEST_BRANCH VARCHAR(100) NOT NULL,
COMMIT_ID_FROM VARCHAR(40) NOT NULL,
COMMIT_ID_TO VARCHAR(40) NOT NULL
);
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -0,0 +1,8 @@
CREATE TABLE WEB_HOOK (
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
URL VARCHAR(200) NOT NULL
);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);

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

@@ -5,8 +5,10 @@ import javax.servlet._
class ScalatraBootstrap extends LifeCycle { class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) { override def init(context: ServletContext) {
context.mount(new IndexController, "/") context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*") context.mount(new SignInController, "/*")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*") context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*") context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*") context.mount(new CreateRepositoryController, "/*")
@@ -16,6 +18,7 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new LabelsController, "/*") context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*") context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*") context.mount(new IssuesController, "/*")
context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*") context.mount(new RepositorySettingsController, "/*")
val dir = new java.io.File(_root_.util.Directory.GitBucketHome) val dir = new java.io.File(_root_.util.Directory.GitBucketHome)

View File

@@ -1,11 +1,10 @@
package app package app
import service._ import service._
import util.{FileUtil, FileUploadUtil, OneselfAuthenticator} import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._ import util.StringUtil._
import util.Directory._ import util.Directory._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
@@ -16,15 +15,16 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
self: SystemSettingsService with AccountService with RepositoryService with ActivityService self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator => 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]) 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) url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "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()))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))) "fileId" -> trim(label("File ID" , optional(text())))
@@ -32,6 +32,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
val editForm = mapping( val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "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")))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))), "fileId" -> trim(label("File ID" , optional(text()))),
@@ -43,12 +44,23 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
*/ */
get("/:userName") { get("/:userName") {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match { params.getOrElse("tab", "repositories") match {
// Public Activity // Public Activity
case "activity" => account.html.activity(x, getActivitiesByUser(userName, true)) case "activity" =>
_root_.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) =>
_root_.account.html.members(account, getGroupMembers(account.userName))
// Repositories // Repositories
case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName))) case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
} }
} getOrElse NotFound } getOrElse NotFound
} }
@@ -74,6 +86,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(account.copy( updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
url = form.url)) url = form.url))
@@ -96,7 +109,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
post("/register", newForm){ form => post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){ 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) updateImage(form.userName, form.fileId, false)
redirect("/signin") redirect("/signin")
} else NotFound } else NotFound

View File

@@ -1,7 +1,9 @@
package app package app
import _root_.util.Directory._ import _root_.util.Directory._
import _root_.util.{FileUploadUtil, FileUtil, Validations} import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{FileUtil, Validations, Keys}
import org.scalatra._ import org.scalatra._
import org.scalatra.json._ import org.scalatra.json._
import org.json4s._ import org.json4s._
@@ -10,7 +12,9 @@ import org.apache.commons.io.FileUtils
import model.Account import model.Account
import scala.Some import scala.Some
import service.AccountService import service.AccountService
import javax.servlet.http.HttpServletRequest import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
@@ -20,73 +24,94 @@ abstract class ControllerBase extends ScalatraFilter
implicit val jsonFormats = DefaultFormats implicit val jsonFormats = DefaultFormats
// Don't set content type via Accept header.
override def format(implicit request: HttpServletRequest) = ""
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request.asInstanceOf[HttpServletRequest]
val httpResponse = response.asInstanceOf[HttpServletResponse]
val context = request.getServletContext.getContextPath
val path = httpRequest.getRequestURI.substring(context.length)
if(path.startsWith("/console/")){
val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
if(account == null){
// Redirect to login form
httpResponse.sendRedirect(context + "/signin?" + path)
} else if(account.isAdmin){
// H2 Console (administrators only)
chain.doFilter(request, response)
} else {
// Redirect to dashboard
httpResponse.sendRedirect(context + "/")
}
} else if(path.startsWith("/git/")){
// Git repository
chain.doFilter(request, response)
} else {
// Scalatra actions
super.doFilter(request, response, chain)
}
}
/** /**
* Returns the context object for the request. * Returns the context object for the request.
*/ */
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
private def currentURL: String = { private def currentURL: String = defining(request.getQueryString){ queryString =>
val queryString = request.getQueryString
request.getRequestURI + (if(queryString != null) "?" + queryString else "") request.getRequestURI + (if(queryString != null) "?" + queryString else "")
} }
private def LoginAccount: Option[Account] = { private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
session.get("LOGIN_ACCOUNT") match {
case Some(x: Account) => Some(x)
case _ => None
}
}
def ajaxGet(path : String)(action : => Any) : Route = { def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){ super.get(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
}
override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = { override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxGet(path, form){ form => super.ajaxGet(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
def ajaxPost(path : String)(action : => Any) : Route = { def ajaxPost(path : String)(action : => Any) : Route =
super.post(path){ super.post(path){
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action action
} }
}
override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = { override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxPost(path, form){ form => super.ajaxPost(path, form){ form =>
request.setAttribute("AJAX", "true") request.setAttribute(Keys.Request.Ajax, "true")
action(form) action(form)
} }
}
protected def NotFound() = { protected def NotFound() =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound(html.error("Not Found"))
} else {
org.scalatra.NotFound() org.scalatra.NotFound()
} else {
org.scalatra.NotFound(html.error("Not Found"))
} }
}
protected def Unauthorized()(implicit context: app.Context) = { protected def Unauthorized()(implicit context: app.Context) =
if(request.getAttribute("AJAX") == null){ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else {
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/")) org.scalatra.Unauthorized(redirect("/"))
} else { } else {
org.scalatra.Unauthorized(redirect("/signin?" + currentURL)) if(request.getMethod.toUpperCase == "POST"){
org.scalatra.Unauthorized(redirect("/signin"))
} else {
org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
}
} }
} else {
org.scalatra.Unauthorized()
} }
}
protected def baseUrl = { protected def baseUrl = defining(request.getRequestURL.toString){ url =>
val url = request.getRequestURL.toString
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
} }
@@ -97,28 +122,36 @@ abstract class ControllerBase extends ScalatraFilter
*/ */
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){ case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
def redirectUrl = if(request.getParameter("redirect") != null){
request.getParameter("redirect")
} else {
currentUrl
}
/** /**
* Get object from cache. * Get object from cache.
* *
* If object has not been cached with the specified key then retrieves by given action. * If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request. * Cached object are available during a request.
*/ */
def cache[A](key: String)(action: => A): A = { def cache[A](key: String)(action: => A): A =
Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse { defining(Keys.Request.Cache(key)){ cacheKey =>
val newObject = action Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
request.setAttribute("cache." + key, newObject) val newObject = action
newObject request.setAttribute(cacheKey, newObject)
newObject
}
} }
}
} }
/** /**
* Base trait for controllers which manages account information. * Base trait for controllers which manages account information.
*/ */
trait AccountManagementControllerBase extends ControllerBase { self: AccountService => trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
self: AccountService =>
protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = { protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if(clearImage){ if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image => getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete() new java.io.File(getUserUploadDir(userName), image).delete()
@@ -126,26 +159,50 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ
} }
} else { } else {
fileId.map { fileId => fileId.map { fileId =>
val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get) val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile( FileUtils.moveFile(
FileUploadUtil.getTemporaryFile(fileId), getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename) new java.io.File(getUserUploadDir(userName), filename)
) )
updateAvatarImage(userName, Some(filename)) updateAvatarImage(userName, Some(filename))
} }
} }
}
protected def uniqueUserName: Constraint = new Constraint(){ protected def uniqueUserName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." } getAccountByUserName(value).map { _ => "User already exists." }
} }
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){ protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getAccountByMailAddress(value) getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) } .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." } .map { _ => "Mail address is already registered." }
} }
}
/**
* Base trait for controllers which needs file uploading feature.
*/
trait FileUploadControllerBase {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
session.getAndRemove[String](Keys.Session.Upload(fileId))
} }

View File

@@ -1,107 +1,193 @@
package app package app
import util.Directory._ import util.Directory._
import util.{JGitUtil, UsersAuthenticator} import util.ControlUtil._
import util._
import service._ import service._
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.apache.commons.io._ import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.lib.PersonIdent
class CreateRepositoryController extends CreateRepositoryControllerBase class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator with UsersAuthenticator with ReadableUsersAuthenticator
/** /**
* Creates new repository. * Creates new repository.
*/ */
trait CreateRepositoryControllerBase extends ControllerBase { trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with WikiService with LabelsService with ActivityService self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
with UsersAuthenticator => with UsersAuthenticator with ReadableUsersAuthenticator =>
case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
val form = mapping( case class ForkRepositoryForm(owner: String, name: String)
val newForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))), "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))), "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())), "isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean())) "createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply) )(RepositoryCreationForm.apply)
val forkForm = mapping(
"owner" -> trim(label("Repository owner", text(required))),
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
/** /**
* Show the new repository form. * Show the new repository form.
*/ */
get("/new")(usersOnly { get("/new")(usersOnly {
html.newrepo() html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
}) })
/** /**
* Create new repository. * Create new repository.
*/ */
post("/new", form)(usersOnly { form => post("/new", newForm)(usersOnly { form =>
val loginAccount = context.loginAccount.get LockUtil.lock(s"${form.owner}/${form.name}/create"){
val loginUserName = loginAccount.userName if(getRepository(form.owner, form.name, baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first // Insert to the database at first
createRepository(form.name, loginUserName, form.description, form.isPrivate) createRepository(form.name, form.owner, form.description, form.isPrivate)
// Insert default labels // Add collaborators for group repository
createLabel(loginUserName, form.name, "bug", "fc2929") if(ownerAccount.isGroupAccount){
createLabel(loginUserName, form.name, "duplicate", "cccccc") getGroupMembers(form.owner).foreach { userName =>
createLabel(loginUserName, form.name, "enhancement", "84b6eb") addCollaborator(form.owner, form.name, userName)
createLabel(loginUserName, form.name, "invalid", "e6e6e6") }
createLabel(loginUserName, form.name, "question", "cc317c") }
createLabel(loginUserName, form.name, "wontfix", "ffffff")
// Create the actual repository // Insert default labels
val gitdir = getRepositoryDir(loginUserName, form.name) insertDefaultLabels(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){ // Create the actual repository
val tmpdir = getInitRepositoryDir(loginUserName, form.name) val gitdir = getRepositoryDir(form.owner, form.name)
try { JGitUtil.initRepository(gitdir)
// Clone the repository
Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
// Create README.md if(form.createReadme){
FileUtils.writeStringToFile(new File(tmpdir, "README.md"), FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
if(form.description.nonEmpty){ // Clone the repository
form.name + "\n" + Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
val git = Git.open(tmpdir) // Create README.md
git.add.addFilepattern("README.md").call FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
git.commit.setMessage("Initial commit").call if(form.description.nonEmpty){
git.push.call form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}, "UTF-8")
} finally { val git = Git.open(tmpdir)
FileUtils.deleteDirectory(tmpdir) git.add.addFilepattern("README.md").call
git.commit
.setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
.setMessage("Initial commit").call
git.push.call
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
} }
// redirect to the repository
redirect(s"/${form.owner}/${form.name}")
} }
// Create Wiki repository
createWikiRepository(loginAccount, form.name)
// Record activity
recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
// redirect to the repository
redirect(s"/${loginUserName}/${form.name}")
}) })
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
createRepository(
repositoryName = repository.name,
userName = loginUserName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
originUserName = Some(originUserName),
parentRepositoryName = Some(repository.name),
parentUserName = Some(repository.owner)
)
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
// insert commit id
using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
JGitUtil.getCommitLog(git, branch) match {
case Right((commits, _)) => commits.foreach { commit =>
if(!existsCommitId(loginUserName, repository.name, commit.id)){
insertCommitId(loginUserName, repository.name, commit.id)
}
}
case Left(_) => ???
}
}
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
}
// redirect to the repository
redirect("/%s/%s".format(loginUserName, repository.name))
}
})
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
createLabel(userName, repositoryName, "enhancement", "84b6eb")
createLabel(userName, repositoryName, "invalid", "e6e6e6")
createLabel(userName, repositoryName, "question", "cc317c")
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
}
/** /**
* Duplicate check for the repository name. * Duplicate check for the repository name.
*/ */
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.") params.get("owner").flatMap { userName =>
getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
}
} }
} }

View File

@@ -0,0 +1,109 @@
package app
import service._
import util.{UsersAuthenticator, Keys}
import util.Implicits._
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None)
})
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
})
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
)
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
//
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
condition),
countIssue(condition, Map.empty, false, repositories: _*),
countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
condition,
filter)
}
private def searchPullRequests(filter: String, repository: Option[String]) = {
import IssuesService._
import PullRequestService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository))
val userName = context.loginAccount.get.userName
val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
dashboard.html.pulls(
pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
page,
countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
getRepositoryNamesOfUser(userName).map { RepoName =>
(userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
}.sortBy(_._3).reverse,
condition,
filter)
}
}

View File

@@ -1,6 +1,7 @@
package app package app
import util.{FileUtil, FileUploadUtil} import _root_.util.{Keys, FileUtil}
import util.ControlUtil._
import org.scalatra._ import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport} import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@@ -9,18 +10,18 @@ import org.apache.commons.io.FileUtils
* Provides Ajax based file upload functionality. * Provides Ajax based file upload functionality.
* *
* This servlet saves uploaded file as temporary file and returns the unique id. * This servlet saves uploaded file as temporary file and returns the unique id.
* You can get uploaded file using [[util.FileUploadUtil#getTemporaryFile()]] with this id. * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/ */
// TODO Remove temporary files at session timeout by session listener. class FileUploadController extends ScalatraServlet
class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport { with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){ post("/image"){
fileParams.get("file") match { fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) => { case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
val fileId = FileUploadUtil.generateFileId FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get) session += Keys.Session.Upload(fileId) -> file.name
session += "upload_" + fileId -> file.name
Ok(fileId) Ok(fileId)
} }
case None => BadRequest case None => BadRequest

View File

@@ -1,21 +1,38 @@
package app package app
import util._
import service._ import service._
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
with RepositoryService with AccountService with SystemSettingsService with ActivityService with RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with SystemSettingsService with ActivityService with AccountService
with UsersAuthenticator =>
trait IndexControllerBase extends ControllerBase { self: RepositoryService
with SystemSettingsService with ActivityService =>
get("/"){ get("/"){
val loginAccount = context.loginAccount val loginAccount = context.loginAccount
html.index(getRecentActivities(), html.index(getRecentActivities(),
getAccessibleRepositories(loginAccount, baseUrl), getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(), loadSystemSettings(),
loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil) loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
) )
} }
} /**
* JSON API for collaborator completion.
*
* TODO Move to other controller?
*/
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
}

View File

@@ -4,7 +4,9 @@ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import IssuesService._ import IssuesService._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator} import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
@@ -57,98 +59,100 @@ trait IssuesControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
val owner = repository.owner defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
val name = repository.name getIssue(owner, name, issueId) map {
val issueId = params("id") issues.html.issue(
getIssue(owner, name, issueId) map {
issues.html.issue(
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
} getOrElse NotFound
}
})
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) :+ owner).sorted,
getMilestones(owner, name), getMilestones(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), hasWritePermission(owner, name, context.loginAccount),
repository) repository)
} getOrElse NotFound }
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
val owner = repository.owner
val name = repository.name
issues.html.create(
(getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name val writable = hasWritePermission(owner, name, context.loginAccount)
val writable = hasWritePermission(owner, name, context.loginAccount) val userName = context.loginAccount.get.userName
val userName = context.loginAccount.get.userName
// insert issue // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None, if(writable) form.assignedUserName else None,
if(writable) form.milestoneId else None) if(writable) form.milestoneId else None)
// insert labels // insert labels
if(writable){ if(writable){
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label => labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId) registerIssueLabel(owner, name, issueId, label.labelId)
}
} }
} }
} }
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
redirect(s"/${owner}/${name}/issues/${issueId}")
} }
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
redirect("/%s/%s/issues/%d".format(owner, name, issueId))
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
getIssue(owner, name, params("id")).map { issue => updateIssue(owner, name, issue.issueId, form.title, form.content)
if(isEditable(owner, name, issue.openedUserName)){ redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
updateIssue(owner, name, issue.issueId, form.title, form.content) } else Unauthorized
redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId)) } getOrElse NotFound
} else Unauthorized }
} getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, Some(form.content), repository)() map { id => handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id)) redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { id => handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id)) redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound } getOrElse NotFound
}) })
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, name) =>
val name = repository.name getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
getComment(owner, name, params("id")).map { comment => updateComment(comment.commentId, form.content)
if(isEditable(owner, name, comment.commentedUserName)){ redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
updateComment(comment.commentId, form.content) } else Unauthorized
redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId)) } getOrElse NotFound
} else Unauthorized }
} getOrElse NotFound
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
@@ -187,17 +191,17 @@ trait IssuesControllerBase extends ControllerBase {
}) })
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
val issueId = params("id").toInt defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) }
}) })
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
@@ -207,128 +211,148 @@ trait IssuesControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
Ok("updated") milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
} getOrElse NotFound
} getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
val action = params.get("value") defining(params.get("value")){ action =>
executeBatch(repository) {
executeBatch(repository) { handleComment(_, None, repository)( _ => action)
handleComment(_, None, repository)( _ => action)
}
})
post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val labelId = params("value").toInt
executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId)
} }
} }
}) })
post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
val value = assignedUserName("value") 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
})
executeBatch(repository) { post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
updateAssignedUserName(repository.owner, repository.name, _, value) defining(assignedUserName("value")){ value =>
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value)
}
} }
}) })
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
val value = milestoneId("value") defining(milestoneId("value")){ value =>
executeBatch(repository) {
executeBatch(repository) { updateMilestoneId(repository.owner, repository.name, _, value)
updateMilestoneId(repository.owner, repository.name, _, value) }
} }
}) })
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") 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 = private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
redirect("/%s/%s/issues".format(repository.owner, repository.name)) redirect(s"/${repository.owner}/${repository.name}/issues")
} }
/** /**
* @see * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/ */
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] = (getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
val owner = repository.owner
val name = repository.name
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue => defining(repository.owner, repository.name){ case (owner, name) =>
val (action, recordActivity) = val userName = context.loginAccount.get.userName
getAction(issue)
.collect { getIssue(owner, name, issueId.toString) map { issue =>
case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _)) val (action, recordActivity) =
case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _)) getAction(issue)
.collect {
case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
t t
} }
.getOrElse(None -> None) .getOrElse(None -> None)
val commentId = content val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get ) .getOrElse ( action.get.capitalize -> action.get )
match { match {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record activity
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) ) content foreach {
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
}
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
commentId // notifications
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issueId, _){
Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
}
}
action foreach {
f.toNotify(repository, issueId, _){
Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
}
issue -> commentId
}
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
val owner = repository.owner defining(repository.owner, repository.name){ case (owner, repoName) =>
val repoName = repository.name val filterUser = Map(filter -> params.getOrElse("userName", ""))
val userName = if(filter != "all") Some(params("userName")) else None val page = IssueSearchCondition.page(request)
val sessionKey = "%s/%s/issues".format(owner, repoName) val sessionKey = Keys.Session.Issues(owner, repoName)
val page = try { // retrieve search condition
val i = params.getOrElse("page", "1").toInt val condition = session.putAndGet(sessionKey,
if(i <= 0) 1 else i if(request.hasQueryString) IssueSearchCondition(request)
} catch { else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
case e: NumberFormatException => 1 )
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
} }
// retrieve search condition
val condition = if(request.getQueryString == null){
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
} else IssueSearchCondition(request)
session.put(sessionKey, condition)
issues.html.list(
searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
getLabels(owner, repoName),
countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
countIssue(owner, repoName, condition, "all", None),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
countIssueGroupByLabels(owner, repoName, condition, filter, userName),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
} }
} }

View File

@@ -51,7 +51,7 @@ trait LabelsControllerBase extends ControllerBase {
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */
private def labelName: Constraint = new Constraint(){ private def labelName: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[^,]+$")){ if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){

View File

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

View File

@@ -0,0 +1,417 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
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._
import org.eclipse.jgit.transport.RefSpec
import org.apache.commons.io.FileUtils
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
import service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import service.RepositoryService.RepositoryTreeNode
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
targetBranch: String,
requestUserName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
})
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
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) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
(getCollaborators(owner, name) :+ owner).sorted,
getMilestonesWithIssueCount(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
}
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
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
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
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)
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 =>
// 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)
// 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
// 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
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}")
}
}
}
}
} getOrElse NotFound
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
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}")
}
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = getForkedCommitId(oldGit, newGit,
originOwner, repository.name, originBranch,
forkedOwner, repository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originOwner, repository.name, oldId.getName,
forkedOwner, repository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
repository.repository.originUserName.map { userName =>
userName :: getForkedRepositories(userName, repository.name)
} getOrElse List(repository.owner),
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
repository,
originRepository,
forkedRepository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
case _ => NotFound
}
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
(getRepository(originOwner, repository.name, baseUrl),
getRepository(forkedOwner, repository.name, baseUrl)) match {
case (Some(originRepository), Some(forkedRepository)) => {
using(
Git.open(getRepositoryDir(originOwner, repository.name)),
Git.open(getRepositoryDir(forkedOwner, repository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
}
}
case _ => NotFound()
}
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = repository.name,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
// TODO Are there more quick way?
LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
val remote = getRepositoryDir(userName, repositoryName)
withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
git.checkout.setName(branch).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
result.getConflicts != null
}
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
/**
* Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
*/
private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
/**
* Returns the identifier of the root commit (or latest merge commit) of the specified branch.
*/
private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
existsCommitId(userName, repositoryName, commit.getName) &&
JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
}.head.id
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
using(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId)
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.toList.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs)
}
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
userName,
page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition, Map.empty, true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}

View File

@@ -6,13 +6,20 @@ import util.{UsersAuthenticator, OwnerAuthenticator}
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport import org.scalatra.FlashMapSupport
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport { trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator => self: RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean) case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping( val optionsForm = mapping(
@@ -20,13 +27,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean())) "isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply) )(OptionsForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String) case class CollaboratorForm(userName: String)
val collaboratorForm = mapping( val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator))) "userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String)
val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
)(WebHookForm.apply)
/** /**
* Redirect to the Options page. * Redirect to the Options page.
*/ */
@@ -45,7 +60,15 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Save the repository options. * Save the repository options.
*/ */
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate) saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
form.defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
)
flash += "info" -> "Repository settings has been updated." flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options") redirect(s"/${repository.owner}/${repository.name}/settings/options")
}) })
@@ -54,22 +77,19 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Display the Collaborators page. * Display the Collaborators page.
*/ */
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository) settings.html.collaborators(
}) getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
/** repository)
* JSON API for collaborator completion.
*/
get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
}) })
/** /**
* Add the collaborator. * Add the collaborator.
*/ */
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
addCollaborator(repository.owner, repository.name, form.userName) if(!getAccountByUserName(repository.owner).get.isGroupAccount){
addCollaborator(repository.owner, repository.name, form.userName)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
@@ -77,10 +97,62 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
* Add the collaborator. * Add the collaborator.
*/ */
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
removeCollaborator(repository.owner, repository.name, params("name")) if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
/**
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Add the web hook URL.
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Delete the web hook URL.
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Send the test request to registered web hook URLs.
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
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!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/** /**
* Display the delete repository page. * Display the delete repository page.
*/ */
@@ -101,19 +173,25 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
}) })
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
}
/** /**
* Provides Constraint to validate the collaborator name. * Provides Constraint to validate the collaborator name.
*/ */
private def collaborator: Constraint = new Constraint(){ private def collaborator: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = { override def validate(name: String, value: String): Option[String] =
val paths = request.getRequestURI.split("/")
getAccountByUserName(value) match { getAccountByUserName(value) match {
case None => Some("User does not exist.") case None => Some("User does not exist.")
case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName)) case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.") => Some("User can access this repository already.")
case _ => None case _ => None
} }
}
} }
} }

View File

@@ -2,7 +2,8 @@ package app
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil} import util.ControlUtil._
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
import service._ import service._
import org.scalatra._ import org.scalatra._
import java.io.File import java.io.File
@@ -10,6 +11,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.api.errors.RefNotFoundException
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator with RepositoryService with AccountService with ReferrerAuthenticator
@@ -37,49 +39,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_) fileList(_)
}) })
/**
* Displays the file list of the repository root and the specified branch.
*/
get("/:owner/:repository/tree/:id")(referrersOnly {
fileList(_, params("id"))
})
/** /**
* Displays the file list of the specified path and branch. * Displays the file list of the specified path and branch.
*/ */
get("/:owner/:repository/tree/:id/*")(referrersOnly { get("/:owner/:repository/tree/*")(referrersOnly { repository =>
fileList(_, params("id"), multiParams("splat").head) val (id, path) = splitPath(repository, multiParams("splat").head)
}) if(path.isEmpty){
fileList(repository, id)
/** } else {
* Displays the commit list of the specified branch. fileList(repository, id, path)
*/
get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
val branchName = params("branch")
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30) match {
case Right((logs, hasNext)) =>
repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
case Left(_) => NotFound
}
} }
}) })
/** /**
* Displays the commit list of the specified resource. * Displays the commit list of the specified resource.
*/ */
get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository => get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val branchName = params("branch") val (branchName, path) = splitPath(repository, multiParams("splat").head)
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "") val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
val page = params.getOrElse("page", "1").toInt
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match { JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
repo.html.commits(path.split("/").toList, branchName, repository, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time) view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext) }, page, hasNext)
@@ -91,12 +73,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Displays the file content of the specified branch or commit. * Displays the file content of the specified branch or commit.
*/ */
get("/:owner/:repository/blob/:id/*")(referrersOnly { repository => get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val id = params("id") // branch name or commit id val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean val raw = params.get("raw").getOrElse("false").toBoolean
val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec @scala.annotation.tailrec
@@ -105,19 +86,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case true => getPathObjectId(path, walk) case true => getPathObjectId(path, walk)
} }
val treeWalk = new TreeWalk(git.getRepository) val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
val objectId = try {
treeWalk.addTree(revCommit.getTree) treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk) getPathObjectId(path, treeWalk)
} finally {
treeWalk.release
} }
if(raw){ if(raw){
// Download // Download
contentType = "application/octet-stream" defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
JGitUtil.getContent(git, objectId, false).get contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else { } else {
// Viewer // Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -127,7 +107,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val content = if(viewer == "other"){ val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){ if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text // text
JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8"))) JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else { } else {
// binary // binary
JGitUtil.ContentInfo("binary", None) JGitUtil.ContentInfo("binary", None)
@@ -148,22 +128,39 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/commit/:id")(referrersOnly { repository => get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id") val id = params("id")
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit), repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
repository, JGitUtil.getDiffs(git, id)) JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, diffs, oldCommitId)
}
}
} }
}) })
/**
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName =>
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen)
}
repo.html.branches(branchInfo, repository)
}
})
/** /**
* Displays tags. * Displays tags.
*/ */
get("/:owner/:repository/tags")(referrersOnly { get("/:owner/:repository/tags")(referrersOnly {
repo.html.tags(_) repo.html.tags(_)
}) })
/** /**
* Download repository contents as an archive. * Download repository contents as an archive.
*/ */
@@ -180,11 +177,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// clone the repository // clone the repository
val cloneDir = new File(workDir, revision) val cloneDir = new File(workDir, revision)
JGitUtil.withGit(Git.cloneRepository using(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString) .setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir) .setDirectory(cloneDir)
.setBranch(revision)
.call){ git => .call){ git =>
// checkout the specified revision // checkout the specified revision
git.checkout.setName(revision).call git.checkout.setName(revision).call
} }
@@ -202,7 +200,29 @@ trait RepositoryViewerControllerBase extends ControllerBase {
BadRequest BadRequest
} }
}) })
get("/:owner/:repository/network/members")(referrersOnly { repository =>
repo.html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
baseUrl),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository)
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} orElse Some(path.split("/")(0)) get
(id, path.substring(id.length).replaceFirst("^/", ""))
}
/** /**
* Provides HTML of the file list. * Provides HTML of the file list.
* *
@@ -215,26 +235,26 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
repo.html.guide(repository) repo.html.guide(repository)
} else { } else {
JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit // get specified commit
revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) => JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId) defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files // get files
val files = JGitUtil.getFileList(git, revision, path) val files = JGitUtil.getFileList(git, revision, path)
// process README.md // process README.md
val readme = files.find(_.name == "README.md").map { file => val readme = files.find(_.name == "README.md").map { file =>
new String(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get, "UTF-8") StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
} }
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme) files, readme)
}
} getOrElse NotFound } getOrElse NotFound
} }
} }
} }
} }

View File

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

View File

@@ -1,8 +1,9 @@
package app package app
import service._ import service._
import util.StringUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import util.Implicits._
import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
@@ -16,27 +17,17 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
)(SignInForm.apply) )(SignInForm.apply)
get("/signin"){ get("/signin"){
val queryString = request.getQueryString val redirect = params.get("redirect")
if(queryString != null && queryString.startsWith("/")){ if(redirect.isDefined && redirect.get.startsWith("/")){
session.setAttribute("REDIRECT", queryString) session.setAttribute(Keys.Session.Redirect, redirect.get)
} }
html.signin(loadSystemSettings()) html.signin(loadSystemSettings())
} }
post("/signin", form){ form => post("/signin", form){ form =>
val account = getAccountByUserName(form.userName) authenticate(loadSystemSettings(), form.userName, form.password) match {
if(account.isEmpty || account.get.password != sha1(form.password)){ case Some(account) => signin(account)
redirect("/signin") case None => redirect("/signin")
} else {
session.setAttribute("LOGIN_ACCOUNT", account.get)
updateLastLoginDate(account.get.userName)
session.get("REDIRECT").map { redirectUrl =>
session.removeAttribute("REDIRECT")
redirect(redirectUrl.asInstanceOf[String])
}.getOrElse {
redirect("/")
}
} }
} }
@@ -45,4 +36,22 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService
redirect("/") redirect("/")
} }
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
}
}.getOrElse {
redirect("/")
}
}
} }

View File

@@ -12,11 +12,30 @@ class SystemSettingsController extends SystemSettingsControllerBase
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with AdminAuthenticator => self: SystemSettingsService with AccountService with AdminAuthenticator =>
private case class SystemSettingsForm(allowAccountRegistration: Boolean)
private val form = mapping( private val form = mapping(
"allowAccountRegistration" -> trim(label("Account registration", boolean())) "allowAccountRegistration" -> trim(label("Account registration", boolean())),
)(SystemSettingsForm.apply) "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"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()))),
"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(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"mailAttribute" -> trim(label("Mail address attribute", text(required)))
)(Ldap.apply))
)(SystemSettings.apply)
get("/admin/system")(adminOnly { get("/admin/system")(adminOnly {
@@ -24,7 +43,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
}) })
post("/admin/system", form)(adminOnly { form => post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(SystemSettings(form.allowAccountRegistration)) saveSystemSettings(form)
flash += "info" -> "System settings has been updated." flash += "info" -> "System settings has been updated."
redirect("/admin/system") redirect("/admin/system")
}) })

View File

@@ -1,67 +1,96 @@
package app package app
import service._ import service._
import util.{FileUploadUtil, FileUtil, AdminAuthenticator} import util.AdminAuthenticator
import util.StringUtil._ import util.StringUtil._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import util.Directory._
import scala.Some
class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator class UserManagementController extends UserManagementControllerBase
with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends AccountManagementControllerBase { trait UserManagementControllerBase extends AccountManagementControllerBase {
self: AccountService with AdminAuthenticator => self: AccountService with RepositoryService with AdminAuthenticator =>
case class UserNewForm(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]) url: Option[String], fileId: Option[String])
case class UserEditForm(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) url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping( case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String])
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
memberNames: Option[String], clearImage: Boolean)
val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "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()))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))) "fileId" -> trim(label("File ID" , optional(text())))
)(UserNewForm.apply) )(NewUserForm.apply)
val editForm = mapping( val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))), "userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))), "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")))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())), "isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))), "url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))), "fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean())) "clearImage" -> trim(label("Clear image" , boolean()))
)(UserEditForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier, uniqueUserName))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text())))
)(NewGroupForm.apply)
val editGroupForm = mapping(
"groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"memberNames" -> trim(label("Member Names" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
)(EditGroupForm.apply)
get("/admin/users")(adminOnly { get("/admin/users")(adminOnly {
admin.users.html.list(getAllUsers()) val users = getAllUsers()
val members = users.collect { case account if(account.isGroupAccount) =>
account.userName -> getGroupMembers(account.userName)
}.toMap
admin.users.html.list(users, members)
}) })
get("/admin/users/_new")(adminOnly { get("/admin/users/_newuser")(adminOnly {
admin.users.html.edit(None) admin.users.html.user(None)
}) })
post("/admin/users/_new", newForm)(adminOnly { form => 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) updateImage(form.userName, form.fileId, false)
redirect("/admin/users") redirect("/admin/users")
}) })
get("/admin/users/:userName/_edit")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") val userName = params("userName")
admin.users.html.edit(getAccountByUserName(userName)) admin.users.html.user(getAccountByUserName(userName))
}) })
post("/admin/users/:name/_edit", editForm)(adminOnly { form => post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { account => getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy( updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(sha1).getOrElse(account.password), password = form.password.map(sha1).getOrElse(account.password),
fullName = form.fullName,
mailAddress = form.mailAddress, mailAddress = form.mailAddress,
isAdmin = form.isAdmin, isAdmin = form.isAdmin,
url = form.url)) url = form.url))
@@ -71,5 +100,46 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
} getOrElse NotFound } getOrElse NotFound
}) })
} get("/admin/users/_newgroup")(adminOnly {
admin.users.html.group(None, Nil)
})
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
createGroup(form.groupName, form.url)
updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
updateImage(form.groupName, form.fileId, false)
redirect("/admin/users")
})
get("/admin/users/:groupName/_editgroup")(adminOnly {
defining(params("groupName")){ groupName =>
admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
}
})
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
getAccountByUserName(groupName).map { account =>
updateGroup(groupName, form.url)
updateGroupMembers(form.groupName, memberNames)
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName)
memberNames.foreach { userName =>
addCollaborator(form.groupName, repositoryName, userName)
}
}
updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users")
} getOrElse NotFound
}
})
post("/admin/users/_usercheck")(adminOnly {
getAccountByUserName(params("userName")).isDefined
})
}

View File

@@ -1,32 +1,39 @@
package app package app
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil} import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ 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 class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase { trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator => 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( val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())) "currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
val editForm = mapping( val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))) "currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
@@ -40,13 +47,13 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
}) })
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository) case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -56,36 +63,60 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val commitId = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), 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 => get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
val commitId = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), 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 => get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
}) })
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
form.content, loginAccount, form.message.getOrElse("")) updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
updateLastActivityDate(repository.owner, repository.name) }
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) 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 { get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -93,24 +124,26 @@ trait WikiControllerBase extends ControllerBase {
}) })
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
val loginAccount = context.loginAccount.get defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, form.content, loginAccount, form.message.getOrElse(""), None)
form.content, context.loginAccount.get, form.message.getOrElse(""))
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)}") 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 => get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, 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 => get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -119,7 +152,7 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository) case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound case Left(_) => NotFound
@@ -128,19 +161,21 @@ trait WikiControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content => val path = multiParams("splat").head
contentType = "application/octet-stream"
content getFileContent(repository.owner, repository.name, path).map { bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
} getOrElse NotFound } getOrElse NotFound
}) })
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.") getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
} }
private def pagename: Constraint = new Constraint(){ private def pagename: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = override def validate(name: String, value: String): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){ if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
@@ -150,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") { object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey) def userName = column[String]("USER_NAME", O PrimaryKey)
def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS") def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD") def password = column[String]("PASSWORD")
def isAdmin = column[Boolean]("ADMINISTRATOR") def isAdmin = column[Boolean]("ADMINISTRATOR")
@@ -12,11 +13,13 @@ object Accounts extends Table[Account]("ACCOUNT") {
def updatedDate = column[java.util.Date]("UPDATED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE") def image = column[String]("IMAGE")
def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _) def groupAccount = column[Boolean]("GROUP_ACCOUNT")
def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
} }
case class Account( case class Account(
userName: String, userName: String,
fullName: String,
mailAddress: String, mailAddress: String,
password: String, password: String,
isAdmin: Boolean, isAdmin: Boolean,
@@ -24,5 +27,6 @@ case class Account(
registeredDate: java.util.Date, registeredDate: java.util.Date,
updatedDate: java.util.Date, updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date], lastLoginDate: Option[java.util.Date],
image: Option[String] image: Option[String],
isGroupAccount: Boolean
) )

View File

@@ -0,0 +1,14 @@
package model
import scala.slick.driver.H2Driver.simple._
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
def groupName = column[String]("GROUP_NAME", O PrimaryKey)
def userName = column[String]("USER_NAME", O PrimaryKey)
def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
}
case class GroupMember(
groupName: String,
userName: String
)

View File

@@ -7,6 +7,11 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
} }
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
def commentCount = column[Int]("COMMENT_COUNT")
def * = userName ~ repositoryName ~ issueId ~ commentCount
}
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate { object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME") def openedUserName = column[String]("OPENED_USER_NAME")
def assignedUserName = column[String]("ASSIGNED_USER_NAME") def assignedUserName = column[String]("ASSIGNED_USER_NAME")
@@ -15,7 +20,8 @@ object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTem
def closed = column[Boolean]("CLOSED") def closed = column[Boolean]("CLOSED")
def registeredDate = column[java.util.Date]("REGISTERED_DATE") def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _) def pullRequest = column[Boolean]("PULL_REQUEST")
def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
} }
@@ -31,4 +37,5 @@ case class Issue(
content: Option[String], content: Option[String],
closed: Boolean, closed: Boolean,
registeredDate: java.util.Date, registeredDate: java.util.Date,
updatedDate: java.util.Date) updatedDate: java.util.Date,
isPullRequest: Boolean)

View File

@@ -0,0 +1,28 @@
package model
import scala.slick.driver.H2Driver.simple._
object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
def branch = column[String]("BRANCH")
def requestUserName = column[String]("REQUEST_USER_NAME")
def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
def requestBranch = column[String]("REQUEST_BRANCH")
def commitIdFrom = column[String]("COMMIT_ID_FROM")
def commitIdTo = column[String]("COMMIT_ID_TO")
def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
}
case class PullRequest(
userName: String,
repositoryName: String,
issueId: Int,
branch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String
)

View File

@@ -9,7 +9,11 @@ object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
def registeredDate = column[java.util.Date]("REGISTERED_DATE") def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE") def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _) def originUserName = column[String]("ORIGIN_USER_NAME")
def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
def parentUserName = column[String]("PARENT_USER_NAME")
def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
} }
@@ -22,5 +26,9 @@ case class Repository(
defaultBranch: String, defaultBranch: String,
registeredDate: java.util.Date, registeredDate: java.util.Date,
updatedDate: java.util.Date, updatedDate: java.util.Date,
lastActivityDate: java.util.Date lastActivityDate: java.util.Date,
originUserName: Option[String],
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String]
) )

View File

@@ -0,0 +1,16 @@
package model
import scala.slick.driver.H2Driver.simple._
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
def url = column[String]("URL")
def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
}
case class WebHook(
userName: String,
repositoryName: String,
url: String
)

View File

@@ -1,5 +1,6 @@
package object model { package object model {
import scala.slick.lifted.MappedTypeMapper import scala.slick.driver.BasicDriver.Implicit._
import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper // java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp]( implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
@@ -7,6 +8,10 @@ package object model {
t => new java.util.Date(t.getTime) t => new java.util.Date(t.getTime)
) )
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
/** /**
* Returns system date. * Returns system date.
*/ */

View File

@@ -3,9 +3,54 @@ package service
import model._ import model._
import scala.slick.driver.H2Driver.simple._ import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession import Database.threadLocalSession
import service.SystemSettingsService.SystemSettings
import util.StringUtil._
import model.GroupMember
import scala.Some
import model.Account
import util.LDAPUtil
import org.slf4j.LoggerFactory
trait AccountService { 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)
} else {
defaultAuthentication(userName, password)
}
/**
* Authenticate by internal database.
*/
private def defaultAuthentication(userName: String, password: String) = {
getAccountByUserName(userName).collect {
case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
} getOrElse None
}
/**
* Authenticate by LDAP.
*/
private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
case Right(mailAddress) => {
// Create or update account by LDAP information
getAccountByUserName(userName) match {
case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
case None => createAccount(userName, "", userName, mailAddress, false, None)
}
getAccountByUserName(userName)
}
case Left(errorMessage) => {
logger.info(s"LDAP Authentication Failed: ${errorMessage}")
defaultAuthentication(userName, password)
}
}
}
def getAccountByUserName(userName: String): Option[Account] = def getAccountByUserName(userName: String): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption Query(Accounts) filter(_.userName is userName.bind) firstOption
@@ -14,24 +59,27 @@ trait AccountService {
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list 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( Accounts insert Account(
userName = userName, userName = userName,
password = password, password = password,
fullName = fullName,
mailAddress = mailAddress, mailAddress = mailAddress,
isAdmin = isAdmin, isAdmin = isAdmin,
url = url, url = url,
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastLoginDate = None, lastLoginDate = None,
image = None) image = None,
isGroupAccount = false)
def updateAccount(account: Account): Unit = def updateAccount(account: Account): Unit =
Accounts Accounts
.filter { a => a.userName is account.userName.bind } .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 ( .update (
account.password, account.password,
account.fullName,
account.mailAddress, account.mailAddress,
account.isAdmin, account.isAdmin,
account.url, account.url,
@@ -44,5 +92,43 @@ trait AccountService {
def updateLastLoginDate(userName: String): Unit = def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate) Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
def createGroup(groupName: String, url: Option[String]): Unit =
Accounts insert Account(
userName = groupName,
password = "",
fullName = groupName,
mailAddress = groupName + "@devnull",
isAdmin = false,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
image = None,
isGroupAccount = true)
def updateGroup(groupName: String, url: Option[String]): Unit =
Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
def updateGroupMembers(groupName: String, members: List[String]): Unit = {
Query(GroupMembers).filter(_.groupName is groupName.bind).delete
members.foreach { userName =>
GroupMembers insert GroupMember (groupName, userName)
}
}
def getGroupMembers(groupName: String): List[String] =
Query(GroupMembers)
.filter(_.groupName is groupName.bind)
.sortBy(_.userName)
.map(_.userName)
.list
def getGroupsByUserName(userName: String): List[String] =
Query(GroupMembers)
.filter(_.userName is userName.bind)
.sortBy(_.groupName)
.map(_.groupName)
.list
} }

View File

@@ -6,23 +6,23 @@ import Database.threadLocalSession
trait ActivityService { trait ActivityService {
def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = { def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
val q = Query(Activities) Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) =>
if(isPublic){
(t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
} else {
(t1.activityUserName is activityUserName.bind)
}
}
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
(if(isPublic){
q filter { case (t1, t2) => (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) }
} else {
q filter { case (t1, t2) => t1.activityUserName is activityUserName.bind }
})
.sortBy { case (t1, t2) => t1.activityId desc }
.map { case (t1, t2) => t1 }
.take(30)
.list
}
def getRecentActivities(): List[Activity] = def getRecentActivities(): List[Activity] =
Query(Activities) Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate is false.bind } .filter { case (t1, t2) => t2.isPrivate is false.bind }
.sortBy { case (t1, t2) => t1.activityId desc } .sortBy { case (t1, t2) => t1.activityId desc }
@@ -52,6 +52,13 @@ trait ActivityService {
Some(title), Some(title),
currentDate) currentDate)
def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"close_issue",
s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate)
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"reopen_issue", "reopen_issue",
@@ -65,7 +72,14 @@ trait ActivityService {
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)), Some(cut(comment, 200)),
currentDate) currentDate)
def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"comment_issue",
s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate)
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) = def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_wiki", "create_wiki",
@@ -73,11 +87,11 @@ trait ActivityService {
Some(pageName), Some(pageName),
currentDate) currentDate)
def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) = def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"edit_wiki", "edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
Some(pageName), Some(pageName + ":" + commitId),
currentDate) currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
@@ -98,15 +112,42 @@ trait ActivityService {
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) = def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName, Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_tag", "create_branch",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None, None,
currentDate) currentDate)
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"fork",
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
None,
currentDate)
def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"open_pullreq",
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(title),
currentDate)
def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"merge_pullreq",
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
Some(message),
currentDate)
def insertCommitId(userName: String, repositoryName: String, commitId: String) = { def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId) 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 = def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined

View File

@@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation import Q.interpolation
import model._ import model._
import util.StringUtil._
import util.Implicits._ import util.Implicits._
import util.StringUtil._
trait IssuesService { trait IssuesService {
import IssuesService._ import IssuesService._
@@ -42,33 +42,28 @@ trait IssuesService {
/** /**
* Returns the count of the search result against issues. * Returns the count of the search result against issues.
* *
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param userName the filter user name required for "assigned" and "created_by" * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @return the count of the search result
*/ */
def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = { def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
// TODO It must be _.length instead of map (_.issueId) list).length. repos: (String, String)*): Int =
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0). Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
// https://github.com/slick/slick/issues/170
(searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
}
/** /**
* Returns the Map which contains issue count for each labels. * Returns the Map which contains issue count for each labels.
* *
* @param owner the repository owner * @param owner the repository owner
* @param repository the repository name * @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param userName the filter user name required for "assigned" and "created_by" * @return the Map which contains issue count for each labels (key is label name, value is issue count)
* @return the Map which contains issue count for each labels (key is label name, value is issue count),
*/ */
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filter: String, userName: Option[String]): Map[String, Int] = { filterUser: Map[String, String]): Map[String, Int] = {
searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName) searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) => .innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
@@ -83,76 +78,100 @@ trait IssuesService {
} }
.toMap .toMap
} }
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t =>
t.userName ~ t.repositoryName
}
.map { case (repo, t) =>
repo ~ t.length
}
.sortBy(_._3 desc)
.list
}
/** /**
* Returns the search result against issues. * Returns the search result against issues.
* *
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filter the filter type ("all", "assigned" or "created_by") * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
* @param userName the filter user name required for "assigned" and "created_by" * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit for pagination * @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(owner: String, repository: String, condition: IssueSearchCondition, def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = { offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
// get issues and comment count // get issues and comment count and labels
val issues = searchIssueQuery(owner, repository, condition, filter, userName) searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
.leftJoin(Query(IssueComments) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.filter { t => .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
(t.byRepository(owner, repository)) && .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
(t.action inSetBind Seq("comment", "close_comment", "reopen_comment")) .map { case (((t1, t2), t3), t4) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
} }
.groupBy { _.issueId } .sortBy(_._4) // labelName
.map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1) .sortBy { case (t1, commentCount, _,_,_) =>
.sortBy { case (t1, t2) => (condition.sort match {
(condition.sort match { case "created" => t1.registeredDate
case "created" => t1.registeredDate case "comments" => commentCount
case "comments" => t2._2 case "updated" => t1.updatedDate
case "updated" => t1.updatedDate }) match {
}) match { case sort => condition.direction match {
case sort => condition.direction match { case "asc" => sort asc
case "asc" => sort asc case "desc" => sort desc
case "desc" => sort desc }
} }
} }
} .drop(offset).take(limit)
.map { case (t1, t2) => (t1, t2._2.ifNull(0)) } .list
.drop(offset).take(limit) .splitWith { (c1, c2) =>
.list c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
// get labels c1._1.issueId == c2._1.issueId
val labels = Query(IssueLabels) }
.innerJoin(Labels).on { (t1, t2) => .map { issues => issues.head match {
t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) case (issue, commentCount, _,_,_) =>
} (issue,
.filter { case (t1, t2) => issues.flatMap { t => t._3.map (
(t1.byRepository(owner, repository)) && Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
(t1.issueId inSetBind (issues.map(_._1.issueId))) )} toList,
} commentCount)
.sortBy { case (t1, t2) => t1.issueId ~ t2.labelName } }} toList
.map { case (t1, t2) => (t1.issueId, t2) }
.list
issues.map { case (issue, commentCount) =>
(issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
}
} }
/** /**
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) = private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
filterUser: Map[String, String], onlyPullRequest: Boolean) =
Query(Issues) filter { t1 => Query(Issues) filter { t1 =>
(t1.byRepository(owner, repository)) && condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed is (condition.state == "closed").bind) && (t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
(t1.assignedUserName is userName.get.bind, filter == "assigned") && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
(t1.openedUserName is userName.get.bind, filter == "created_by") && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
(t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
(t1.pullRequest is true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -164,7 +183,7 @@ trait IssuesService {
} }
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int]) = assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
// next id number // next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
.firstOption.filter { id => .firstOption.filter { id =>
@@ -179,7 +198,8 @@ trait IssuesService {
content, content,
false, false,
currentDate, currentDate,
currentDate) currentDate,
isPullRequest)
// increment issue id // increment issue id
IssueId IssueId
@@ -237,6 +257,60 @@ trait IssuesService {
} }
.update (closed, currentDate) .update (closed, currentDate)
/**
* Search issues by keyword.
*
* @param owner the repository owner
* @param repository the repository name
* @param query the keywords separated by whitespace.
* @return issues with comment count and matched content of issue or comment
*/
def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
import scala.slick.driver.H2Driver.likeEncode
val keywords = splitWords(query.toLowerCase)
// Search Issue
val issues = Issues
.innerJoin(IssueOutline).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.filter { case (t1, t2) =>
keywords.map { keyword =>
(t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
(t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
} .reduceLeft(_ && _)
}
.map { case (t1, t2) =>
(t1, 0, t1.content.?, t2.commentCount)
}
// Search IssueComment
val comments = IssueComments
.innerJoin(Issues).on { case (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
.innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
}
.filter { case ((t1, t2), t3) =>
keywords.map { query =>
t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
}.reduceLeft(_ && _)
}
.map { case ((t1, t2), t3) =>
(t2, t1.commentId, t1.content.?, t3.commentCount)
}
issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId
}.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
}.map { _.head match {
case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
}
}.toList
}
} }
object IssuesService { object IssuesService {
@@ -247,6 +321,7 @@ object IssuesService {
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
repo: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc"){
@@ -258,6 +333,7 @@ object IssuesService {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
})}, })},
repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&") Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -274,12 +350,21 @@ object IssuesService {
def apply(request: HttpServletRequest): IssueSearchCondition = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
param(request, "milestone").map(_ match { param(request, "milestone").map{
case "none" => None 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, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
} }
} }

View File

@@ -0,0 +1,54 @@
package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._
import util.ControlUtil._
trait PullRequestService { self: IssuesService =>
import PullRequestService._
def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
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] =
Query(PullRequests)
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t2.closed is closed.bind) &&
(t1.userName is owner.bind) &&
(t1.repositoryName is repository.get.bind, repository.isDefined)
}
.groupBy { case (t1, t2) => t2.openedUserName }
.map { case (userName, t) => userName ~ t.length }
.sortBy(_._2 desc)
.list
.map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
commitIdFrom: String, commitIdTo: String): Unit =
PullRequests insert (PullRequest(
originUserName,
originRepositoryName,
issueId,
originBranch,
requestUserName,
requestRepositoryName,
requestBranch,
commitIdFrom,
commitIdTo))
}
object PullRequestService {
val PullRequestLimit = 25
case class PullRequestCount(userName: String, count: Int)
}

View File

@@ -0,0 +1,126 @@
package service
import util.{FileUtil, StringUtil, JGitUtil}
import util.Directory._
import util.ControlUtil._
import model.Issue
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.collection.mutable.ListBuffer
import org.eclipse.jgit.lib.FileMode
import org.eclipse.jgit.api.Git
trait
RepositorySearchService { self: IssuesService =>
import RepositorySearchService._
def countIssues(owner: String, repository: String, query: String): Int =
searchIssuesByKeyword(owner, repository, query).length
def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
IssueSearchResult(
issue.issueId,
issue.title,
issue.openedUserName,
issue.registeredDate,
commentCount,
getHighlightText(content, query)._1)
}
def countFiles(owner: String, repository: String, query: String): Int =
using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
}
def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
using(Git.open(getRepositoryDir(owner, repository))){ git =>
if(JGitUtil.isEmpty(git)){
Nil
} else {
val files = searchRepositoryFiles(git, query)
val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path,
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)
}
}
}
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve("HEAD")
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.setRecursive(true)
treeWalk.addTree(revCommit.getTree)
val keywords = StringUtil.splitWords(query.toLowerCase)
val list = new ListBuffer[(String, String)]
while (treeWalk.next()) {
if(treeWalk.getFileMode(0) != FileMode.TREE){
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
if(FileUtil.isText(bytes)){
val text = StringUtil.convertFromByteArray(bytes)
val lowerText = text.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
list.append((treeWalk.getPathString, text))
}
}
}
}
}
treeWalk.release
revWalk.release
list.toList
}
}
object RepositorySearchService {
val CodeLimit = 10
val IssueLimit = 10
def getHighlightText(content: String, query: String): (String, Int) = {
val keywords = StringUtil.splitWords(query.toLowerCase)
val lowerText = content.toLowerCase
val indices = keywords.map(lowerText.indexOf _)
if(!indices.exists(_ < 0)){
val lineNumber = content.substring(0, indices.min).split("\n").size - 1
val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
.replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
"<span class=\"highlight\">$1</span>")
(highlightText, lineNumber + 1)
} else {
(content.split("\n").take(5).mkString("\n"), 1)
}
}
case class SearchResult(
files : List[(String, String)],
issues: List[(Issue, Int, String)])
case class IssueSearchResult(
issueId: Int,
title: String,
openedUserName: String,
registeredDate: java.util.Date,
commentCount: Int,
highlightText: String)
case class FileSearchResult(
path: String,
lastModified: java.util.Date,
highlightText: String,
highlightLineNumber: Int)
}

View File

@@ -15,19 +15,27 @@ trait RepositoryService { self: AccountService =>
* @param userName the user name of the repository owner * @param userName the user name of the repository owner
* @param description the repository description * @param description the repository description
* @param isPrivate the repository type (private is true, otherwise false) * @param isPrivate the repository type (private is true, otherwise false)
* @param originRepositoryName specify for the forked repository. (default is None)
* @param originUserName specify for the forked repository. (default is None)
*/ */
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = { def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
Repositories insert Repositories insert
Repository( Repository(
userName = userName, userName = userName,
repositoryName = repositoryName, repositoryName = repositoryName,
isPrivate = isPrivate, isPrivate = isPrivate,
description = description, description = description,
defaultBranch = "master", defaultBranch = "master",
registeredDate = currentDate, registeredDate = currentDate,
updatedDate = currentDate, updatedDate = currentDate,
lastActivityDate = currentDate) lastActivityDate = currentDate,
originUserName = originUserName,
originRepositoryName = originRepositoryName,
parentUserName = parentUserName,
parentRepositoryName = parentRepositoryName)
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
@@ -39,8 +47,10 @@ trait RepositoryService { self: AccountService =>
Labels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete IssueComments .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete Issues .filter(_.byRepository(userName, repositoryName)).delete
PullRequests .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete
} }
@@ -53,39 +63,6 @@ trait RepositoryService { self: AccountService =>
def getRepositoryNamesOfUser(userName: String): List[String] = def getRepositoryNamesOfUser(userName: String): List[String] =
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/**
* Returns the list of specified user's repositories information.
*
* @param userName the user name
* @param baseUrl the base url of this application
* @param loginUserName the logged in user name
* @return the list of repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
val q1 = Repositories
.filter { t => t.userName is userName.bind }
.map { r => r }
val q2 = Collaborators
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
.map { case (t1, t2) => t2 }
def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
loginUserName match {
case Some(x) => (t.isPrivate is false.bind) || (
(t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
}.exists)))
case None => (t.isPrivate is false.bind)
}
}
q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
}
/** /**
* Returns the specified repository information. * Returns the specified repository information.
* *
@@ -96,34 +73,69 @@ trait RepositoryService { self: AccountService =>
*/ */
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository) // for getting issue count and pull request count
val issues = Query(Issues).filter { t =>
t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
}.map(_.pullRequest).list
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
issues.size,
issues.filter(_ == true).size,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
}
def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
Query(Repositories).filter { t1 =>
(t1.userName is userName.bind) ||
(Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
} }
} }
/** /**
* Returns the list of accessible repositories information for the specified account user. * Returns the list of visible repositories for the specified user.
* * If repositoryUserName is given then filters results by repository owner.
* @param account the account *
* @param loginAccount the logged in account
* @param baseUrl the base url of this application * @param baseUrl the base url of this application
* @return the repository informations which is sorted in descending order of lastActivityDate. * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @return the repository information which is sorted in descending order of lastActivityDate.
*/ */
def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = { def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
(loginAccount match {
def newRepositoryInfo(repository: Repository): RepositoryInfo = {
new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
}
(account match {
// for Administrators // for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories) case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users // for Normal Users
case Some(x) if(!x.isAdmin) => case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) || Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
(Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists) (Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
} }
// for Guests // for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind) case None => Query(Repositories) filter(_.isPrivate is false.bind)
}).sortBy(_.lastActivityDate desc).list.map(newRepositoryInfo _) }).filter { t =>
repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
repository,
getForkedCount(
repository.originUserName.getOrElse(repository.userName),
repository.originRepositoryName.getOrElse(repository.repositoryName)
))
}
} }
/** /**
@@ -161,6 +173,15 @@ trait RepositoryService { self: AccountService =>
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit = def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
* Remove all collaborators from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
*/
def removeCollaborators(userName: String, repositoryName: String): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
* Returns the list of collaborators name which is sorted with ascending order. * Returns the list of collaborators name which is sorted with ascending order.
* *
@@ -180,17 +201,39 @@ trait RepositoryService { self: AccountService =>
} }
} }
private def getForkedCount(userName: String, repositoryName: String): Int =
Query(Repositories.filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}.length).first
def getForkedRepositories(userName: String, repositoryName: String): List[String] =
Query(Repositories).filter { t =>
(t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
}
.sortBy(_.userName asc).map(_.userName).list
} }
object RepositoryService { object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
def this(repo: JGitUtil.RepositoryInfo, model: Repository) = { /**
this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags) * Creates instance with issue count and pull request count.
} */
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
} }
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
} }

View File

@@ -1,6 +1,7 @@
package service package service
import model._ import model._
import service.SystemSettingsService.SystemSettings
/** /**
* This service is used for a view helper mainly. * This service is used for a view helper mainly.
@@ -10,6 +11,11 @@ import model._
*/ */
trait RequestCache { trait RequestCache {
def getSystemSettings()(implicit context: app.Context): SystemSettings =
context.cache("system_settings"){
new SystemSettingsService {}.loadSystemSettings()
}
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = { def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
new IssuesService {}.getIssue(userName, repositoryName, issueId) new IssuesService {}.getIssue(userName, repositoryName, issueId)

View File

@@ -1,40 +1,152 @@
package service package service
import util.Directory._ import util.Directory._
import SystemSettingsService._ import util.ControlUtil._
import SystemSettingsService._
trait SystemSettingsService {
trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = {
val props = new java.util.Properties() def saveSystemSettings(settings: SystemSettings): Unit = {
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) defining(new java.util.Properties()){ props =>
props.store(new java.io.FileOutputStream(GitBucketConf), null) props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
} props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
if(settings.notification) {
def loadSystemSettings(): SystemSettings = { settings.smtp.foreach { smtp =>
val props = new java.util.Properties() props.setProperty(SmtpHost, smtp.host)
if(GitBucketConf.exists){ smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
props.load(new java.io.FileInputStream(GitBucketConf)) smtp.user.foreach(props.setProperty(SmtpUser, _))
} smtp.password.foreach(props.setProperty(SmtpPassword, _))
SystemSettings(getBoolean(props, "allow_account_registration")) smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
} smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
} }
}
object SystemSettingsService { props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
if(settings.ldapAuthentication){
case class SystemSettings(allowAccountRegistration: Boolean) settings.ldap.map { ldap =>
props.setProperty(LdapHost, ldap.host)
private val AllowAccountRegistration = "allow_account_registration" ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = { ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
val value = props.getProperty(key) props.setProperty(LdapBaseDN, ldap.baseDN)
if(value == null || value.isEmpty){ props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
default props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
} else { }
value.toBoolean }
} props.store(new java.io.FileOutputStream(GitBucketConf), null)
} }
}
}
def loadSystemSettings(): SystemSettings = {
defining(new java.util.Properties()){ props =>
if(GitBucketConf.exists){
props.load(new java.io.FileInputStream(GitBucketConf))
}
SystemSettings(
getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
if(getValue(props, Notification, false)){
Some(Smtp(
getValue(props, SmtpHost, ""),
getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
getOptionValue(props, SmtpUser, None),
getOptionValue(props, SmtpPassword, None),
getOptionValue[Boolean](props, SmtpSsl, None),
getOptionValue(props, SmtpFromAddress, None),
getOptionValue(props, SmtpFromName, None)))
} else {
None
},
getValue(props, LdapAuthentication, false),
if(getValue(props, LdapAuthentication, false)){
Some(Ldap(
getValue(props, LdapHost, ""),
getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
getOptionValue(props, LdapBindDN, None),
getOptionValue(props, LdapBindPassword, None),
getValue(props, LdapBaseDN, ""),
getValue(props, LdapUserNameAttribute, ""),
getValue(props, LdapMailAddressAttribute, "")))
} else {
None
}
)
}
}
}
object SystemSettingsService {
import scala.reflect.ClassTag
case class SystemSettings(
allowAccountRegistration: Boolean,
gravatar: Boolean,
notification: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap])
case class Ldap(
host: String,
port: Option[Int],
bindDN: Option[String],
bindPassword: Option[String],
baseDN: String,
userNameAttribute: String,
mailAttribute: String)
case class Smtp(
host: String,
port: Option[Int],
user: Option[String],
password: Option[String],
ssl: Option[Boolean],
fromAddress: Option[String],
fromName: Option[String])
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val SmtpHost = "smtp.host"
private val SmtpPort = "smtp.port"
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"
private val LdapBindDN = "ldap.bindDN"
private val LdapBindPassword = "ldap.bind_password"
private val LdapBaseDN = "ldap.baseDN"
private val LdapUserNameAttribute = "ldap.username_attribute"
private val LdapMailAddressAttribute = "ldap.mail_attribute"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else convertType(value).asInstanceOf[A]
}
private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
defining(props.getProperty(key)){ value =>
if(value == null || value.isEmpty) default
else Some(convertType(value)).asInstanceOf[Option[A]]
}
private def convertType[A: ClassTag](value: String) =
defining(implicitly[ClassTag[A]].runtimeClass){ c =>
if(c == classOf[Boolean]) value.toBoolean
else if(c == classOf[Int]) value.toInt
else value
}
}

View File

@@ -0,0 +1,143 @@
package service
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
import model._
import org.slf4j.LoggerFactory
import service.RepositoryService.RepositoryInfo
import util.JGitUtil
import org.eclipse.jgit.diff.DiffEntry
import util.JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.protocol.HTTP
import org.apache.http.NameValuePair
trait WebHookService {
import WebHookService._
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String): List[WebHook] =
Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
def addWebHookURL(owner: String, repository: String, url :String): Unit =
WebHooks.insert(WebHook(owner, repository, url))
def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
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.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
if(webHookURLs.nonEmpty){
val json = write(payload)
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl =>
val f = future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
httpClient.execute(httpPost)
httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}")
}
f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
}
f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
}
}
}
logger.debug("end callWebHook")
}
}
object WebHookService {
case class WebHookPayload(
ref: String,
commits: List[WebHookCommit],
repository: WebHookRepository)
object WebHookPayload {
def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
WebHookPayload(
refName,
commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.time.toString,
url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser(
name = commit.committer,
email = commit.mailAddress
)
)
}.toList,
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.url,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,
`private` = repositoryInfo.repository.isPrivate,
owner = WebHookUser(
name = repositoryOwner.userName,
email = repositoryOwner.mailAddress
)
)
)
}
case class WebHookCommit(
id: String,
message: String,
timestamp: String,
url: String,
added: List[String],
removed: List[String],
modified: List[String],
author: WebHookUser)
case class WebHookRepository(
name: String,
url: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
owner: WebHookUser)
case class WebHookUser(
name: String,
email: String)
}

View File

@@ -4,11 +4,11 @@ import java.io.File
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import util.JGitUtil.DiffInfo import util.{StringUtil, Directory, JGitUtil, LockUtil}
import util.{Directory, JGitUtil} import util.ControlUtil._
import org.eclipse.jgit.lib.RepositoryBuilder
import org.eclipse.jgit.treewalk.CanonicalTreeParser import org.eclipse.jgit.treewalk.CanonicalTreeParser
import java.util.concurrent.ConcurrentHashMap import org.eclipse.jgit.diff.DiffFormatter
import org.eclipse.jgit.api.errors.PatchApplyException
object WikiService { object WikiService {
@@ -19,8 +19,9 @@ object WikiService {
* @param content the page content * @param content the page content
* @param committer the last committer * @param committer the last committer
* @param time the last modified time * @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. * The model for wiki page history.
@@ -32,65 +33,35 @@ object WikiService {
*/ */
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
/**
* lock objects
*/
private val locks = new ConcurrentHashMap[String, AnyRef]()
/**
* Returns the lock object for the specified repository.
*/
private def getLockObject(owner: String, repository: String): AnyRef = synchronized {
val key = owner + "/" + repository
if(!locks.containsKey(key)){
locks.put(key, new AnyRef())
}
locks.get(key)
}
/**
* Synchronizes a given function which modifies the working copy of the wiki repository.
*
* @param owner the repository owner
* @param repository the repository name
* @param f the function which modifies the working copy of the wiki repository
* @tparam T the return type of the given function
* @return the result of the given function
*/
def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f)
} }
trait WikiService { trait WikiService {
import WikiService._ import WikiService._
def createWikiRepository(owner: model.Account, repository: String): Unit = { def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
lock(owner.userName, repository){ LockUtil.lock(s"${owner}/${repository}/wiki"){
val dir = Directory.getWikiRepositoryDir(owner.userName, repository) defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
if(!dir.exists){ if(!dir.exists){
try { try {
JGitUtil.initRepository(dir) JGitUtil.initRepository(dir)
saveWikiPage(owner.userName, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", owner, "Initial Commit") saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} finally { } finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge' // once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository)) FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
}
} }
} }
} }
}
/** /**
* Returns the wiki page. * Returns the wiki page.
*/ */
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
try { optionIf(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => 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)
} }
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
} }
} }
} }
@@ -98,9 +69,9 @@ trait WikiService {
/** /**
* Returns the content of the specified file. * Returns the content of the specified file.
*/ */
def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = { def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
try { optionIf(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/') val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index) val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1) val fileName = if(index < 0) path else path.substring(index + 1)
@@ -108,57 +79,119 @@ trait WikiService {
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes git.getRepository.open(file.id).getBytes
} }
} catch {
// TODO no commit, but it should not judge by exception.
case e: NullPointerException => None
} }
} }
}
/** /**
* Returns the list of wiki page names. * Returns the list of wiki page names.
*/ */
def getWikiPageList(owner: String, repository: String): List[String] = { def getWikiPageList(owner: String, repository: String): List[String] = {
JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".") using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
.filter(_.name.endsWith(".md")) JGitUtil.getFileList(git, "master", ".")
.map(_.name.replaceFirst("\\.md$", "")) .filter(_.name.endsWith(".md"))
.sortBy(x => x) .map(_.name.replaceFirst("\\.md$", ""))
.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. * Save the wiki page.
*/ */
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Unit = { content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
lock(owner, repository){ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
val workDir = Directory.getWikiWorkDir(owner, repository) // clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository) cloneOrPullWorkingCopy(workDir, owner, repository)
// write as file // write as file
JGitUtil.withGit(workDir){ git => using(Git.open(workDir)){ git =>
val file = new File(workDir, newPageName + ".md") defining(new File(workDir, newPageName + ".md")){ file =>
val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ // new page
FileUtils.writeStringToFile(file, content, "UTF-8") val created = !file.exists
git.add.addFilepattern(file.getName).call
true
} else {
false
}
// delete file // created or updated
val deleted = if(currentPageName != "" && currentPageName != newPageName){ val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
git.rm.addFilepattern(currentPageName + ".md").call FileUtils.writeStringToFile(file, content, "UTF-8")
true git.add.addFilepattern(file.getName).call
} else { }
false
}
// commit and push // delete file
if(added || deleted){ val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call git.rm.addFilepattern(currentPageName + ".md").call
git.push.call }
// commit and push
optionIf(added || deleted){
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)
}
}
}
} }
} }
} }
@@ -167,56 +200,38 @@ trait WikiService {
/** /**
* Delete the wiki page. * Delete the wiki page.
*/ */
def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = { def deleteWikiPage(owner: String, repository: String, pageName: String,
lock(owner, repository){ committer: String, mailAddress: String, message: String): Unit = {
// clone working copy LockUtil.lock(s"${owner}/${repository}/wiki"){
val workDir = Directory.getWikiWorkDir(owner, repository) defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
cloneOrPullWorkingCopy(workDir, owner, repository) // clone working copy
cloneOrPullWorkingCopy(workDir, owner, repository)
// delete file // delete file
new File(workDir, pageName + ".md").delete new File(workDir, pageName + ".md").delete
JGitUtil.withGit(workDir){ git => using(Git.open(workDir)){ git =>
git.rm.addFilepattern(pageName + ".md").call git.rm.addFilepattern(pageName + ".md").call
// commit and push // commit and push
// TODO committer's mail address git.commit.setCommitter(committer, mailAddress).setMessage(message).call
git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call git.push.call
git.push.call }
} }
} }
} }
/**
* Returns differences between specified commits.
*/
def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = {
// get diff between specified commit and its previous commit
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}"))
val newTreeIter = new CanonicalTreeParser
newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}"))
import scala.collection.JavaConverters._
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8")))
}.toList
}
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){ if(!workDir.exists){
Git.cloneRepository Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString) .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir) .setDirectory(workDir)
.call .call
} else { .getRepository
Git.open(workDir).pull.call .close
} else using(Git.open(workDir)){ git =>
git.pull.call
} }
} }
} }

View File

@@ -1,12 +1,14 @@
package servlet package servlet
import java.io.File import java.io.File
import java.sql.Connection import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import javax.servlet.ServletContextEvent import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Directory import util.Directory._
import util.ControlUtil._
import org.eclipse.jgit.api.Git
object AutoUpdate { object AutoUpdate {
@@ -26,15 +28,14 @@ object AutoUpdate {
*/ */
def update(conn: Connection): Unit = { def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
if(in != null){ using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
val sql = IOUtils.toString(in, "UTF-8") if(in != null){
val stmt = conn.createStatement() val sql = IOUtils.toString(in, "UTF-8")
try { using(conn.createStatement()){ stmt =>
logger.debug(sqlPath + "=" + sql) logger.debug(sqlPath + "=" + sql)
stmt.executeUpdate(sql) stmt.executeUpdate(sql)
} finally { }
stmt.close()
} }
} }
} }
@@ -49,26 +50,32 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){ new Version(1, 3){
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {
super.update(conn) super.update(conn)
// Fix wiki repository configuration // Fix wiki repository configuration
val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY") using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
while(rs.next){ while(rs.next){
val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")) using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository defining(git.getRepository.getConfig){ config =>
val config = repository.getConfig if(!config.getBoolean("http", "receivepack", false)){
if(!config.getBoolean("http", "receivepack", false)){ config.setBoolean("http", null, "receivepack", true)
config.setBoolean("http", null, "receivepack", true) config.save
config.save }
}
}
} }
repository.close
} }
} }
}, },
Version(1, 2), Version(1, 2),
Version(1, 1), Version(1, 1),
Version(1, 0) Version(1, 0),
Version(0, 0)
) )
/** /**
@@ -79,7 +86,7 @@ object AutoUpdate {
/** /**
* The version file (GITBUCKET_HOME/version). * The version file (GITBUCKET_HOME/version).
*/ */
val versionFile = new File(Directory.GitBucketHome, "version") val versionFile = new File(GitBucketHome, "version")
/** /**
* Returns the current version from the version file. * Returns the current version from the version file.
@@ -103,35 +110,50 @@ 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._ import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = { override def contextInitialized(event: ServletContextEvent): Unit = {
super.contextInitialized(event) org.h2.Driver.load()
logger.debug("H2 started") event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
logger.debug("Start schema update") logger.debug("Start schema update")
val conn = getConnection() defining(getConnection(event.getServletContext)){ conn =>
try { try {
val currentVersion = getCurrentVersion() defining(getCurrentVersion()){ currentVersion =>
if(currentVersion == headVersion){ if(currentVersion == headVersion){
logger.debug("No update") logger.debug("No update")
} else { } else if(!versions.contains(currentVersion)){
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") } else {
conn.commit() versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString) FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
} conn.commit()
} catch { logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
case ex: Throwable => { }
logger.error("Failed to schema update", ex) }
conn.rollback() } catch {
case ex: Throwable => {
logger.error("Failed to schema update", ex)
ex.printStackTrace()
conn.rollback()
}
} }
} }
logger.debug("End schema update") 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

@@ -2,14 +2,16 @@ package servlet
import javax.servlet._ import javax.servlet._
import javax.servlet.http._ import javax.servlet.http._
import util.StringUtil._ import service.{SystemSettingsService, AccountService, RepositoryService}
import service.{AccountService, RepositoryService}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import util.Implicits._
import util.ControlUtil._
import util.Keys
/** /**
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]]. * Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
*/ */
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService { class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter]) private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
@@ -26,28 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
try { try {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
val repositoryOwner = paths(2) getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
val repositoryName = paths(3).replaceFirst("\\.git$", "") case Some(repository) => {
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match { !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
case Some(repository) => { chain.doFilter(req, wrappedResponse)
if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){ } else {
chain.doFilter(req, wrappedResponse) request.getHeader("Authorization") match {
} else { case null => requireAuth(response)
request.getHeader("Authorization") match { case auth => decodeAuthHeader(auth).split(":") match {
case null => requireAuth(response) case Array(username, password) if(isWritableUser(username, password, repository)) => {
case auth => decodeAuthHeader(auth).split(":") match { request.setAttribute(Keys.Request.UserName, username)
case Array(username, password) if(isWritableUser(username, password, repository)) => { chain.doFilter(req, wrappedResponse)
request.setAttribute("USER_NAME", username) }
chain.doFilter(req, wrappedResponse) case _ => requireAuth(response)
} }
case _ => requireAuth(response)
} }
} }
} }
case None => {
logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} }
case None => response.sendError(HttpServletResponse.SC_NOT_FOUND)
} }
} catch { } catch {
case ex: Exception => { case ex: Exception => {
@@ -57,12 +61,12 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = { private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
getAccountByUserName(username).map { account => authenticate(loadSystemSettings(), username, password) match {
account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account)) case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
} getOrElse false case None => false
} }
private def requireAuth(response: HttpServletResponse): Unit = { private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED) response.sendError(HttpServletResponse.SC_UNAUTHORIZED)

View File

@@ -9,8 +9,13 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig import javax.servlet.ServletConfig
import javax.servlet.ServletContext import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import util.{JGitUtil, Directory} import util.{Keys, JGitUtil, Directory}
import util.ControlUtil._
import util.Implicits._
import service._ import service._
import WebHookService._
import org.eclipse.jgit.api.Git
import util.JGitUtil.CommitInfo
/** /**
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
@@ -24,7 +29,7 @@ class GitRepositoryServlet extends GitServlet {
override def init(config: ServletConfig): Unit = { override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory()) setReceivePackFactory(new GitBucketReceivePackFactory())
// TODO are there any other ways...? // TODO are there any other ways...?
super.init(new ServletConfig(){ super.init(new ServletConfig(){
def getInitParameter(name: String): String = name match { def getInitParameter(name: String): String = name match {
@@ -33,12 +38,14 @@ class GitRepositoryServlet extends GitServlet {
case name => config.getInitParameter(name) case name => config.getInitParameter(name)
} }
def getInitParameterNames(): java.util.Enumeration[String] = { def getInitParameterNames(): java.util.Enumeration[String] = {
config.getInitParameterNames config.getInitParameterNames
} }
def getServletContext(): ServletContext = config.getServletContext def getServletContext(): ServletContext = config.getServletContext
def getServletName(): String = config.getServletName def getServletName(): String = config.getServletName
}); });
super.init(config)
} }
} }
@@ -49,68 +56,102 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
override def create(request: HttpServletRequest, db: Repository): ReceivePack = { override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db) val receivePack = new ReceivePack(db)
val userName = request.getAttribute("USER_NAME").asInstanceOf[String] val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI) logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName) logger.debug("userName:" + userName)
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
val owner = paths(2) val owner = paths(1)
val repository = paths(3).replaceFirst("\\.git$", "") val repository = paths(2).replaceFirst("\\.git$", "")
val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
logger.debug("repository:" + owner + "/" + repository)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName)) logger.debug("repository:" + owner + "/" + repository)
receivePack logger.debug("baseURL:" + baseURL)
receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
receivePack
}
} }
} }
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService { with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git => using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command => commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
// apply issue comment
val newCommits = commits.flatMap { commit => // Extract new commit and apply issue comment
if(!existsCommitId(owner, repository, commit.id)){ val newCommits = if(commits.size > 1000){
insertCommitId(owner, repository, commit.id) val existIds = getAllCommitIds(owner, repository)
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData => commits.flatMap { commit =>
val issueId = matchData.group(2) optionIf(!existIds.contains(commit.id)){
if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){ createIssueComment(commit)
createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit") Some(commit)
}
} }
Some(commit) }
} else None } else {
}.toList 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 // record activity
if(refName(1) == "heads"){ if(refName(1) == "heads"){
command.getType match { command.getType match {
case ReceiveCommand.Type.CREATE => { case ReceiveCommand.Type.CREATE => {
recordCreateBranchActivity(owner, repository, userName, refName(2)) recordCreateBranchActivity(owner, repository, userName, branchName)
recordPushActivity(owner, repository, userName, refName(2), newCommits) recordPushActivity(owner, repository, userName, branchName, newCommits)
} }
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits) case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
case _ => case _ =>
} }
} else if(refName(1) == "tags"){ } else if(refName(1) == "tags"){
command.getType match { 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 _ => case _ =>
} }
} }
// call web hook
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, webHookURLs, payload)
}
} }
} }
// update repository last modified time. // update repository last modified time.
updateLastActivityDate(owner, repository) 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")
}
}
}
} }

View File

@@ -1,17 +1,15 @@
package servlet package servlet
import util.FileUploadUtil
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
import app.FileUploadControllerBase
/** /**
* Removes session associated temporary files when session is destroyed. * Removes session associated temporary files when session is destroyed.
*/ */
class SessionCleanupListener extends HttpSessionListener { class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {} def sessionCreated(se: HttpSessionEvent): Unit = {}
def sessionDestroyed(se: HttpSessionEvent): Unit = { def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
FileUploadUtil.removeTemporaryFiles()(se.getSession)
}
} }

View File

@@ -3,7 +3,6 @@ package servlet
import javax.servlet._ import javax.servlet._
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import scala.slick.session.Database
/** /**
* Controls the transaction with the open session in view pattern. * Controls the transaction with the open session in view pattern.
@@ -21,15 +20,19 @@ class TransactionFilter extends Filter {
// assets don't need transaction // assets don't need transaction
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {
val context = req.getServletContext Database(req.getServletContext) withTransaction {
Database.forURL(context.getInitParameter("db.url"), logger.debug("begin transaction")
context.getInitParameter("db.user"),
context.getInitParameter("db.password")) withTransaction {
logger.debug("TODO begin transaction")
chain.doFilter(req, res) chain.doFilter(req, res)
logger.debug("TODO end transaction") logger.debug("end transaction")
} }
} }
} }
} }
object Database {
def apply(context: ServletContext): scala.slick.session.Database =
scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
context.getInitParameter("db.user"),
context.getInitParameter("db.password"))
}

View File

@@ -3,6 +3,8 @@ package util
import app.ControllerBase import app.ControllerBase
import service._ import service._
import RepositoryService.RepositoryInfo import RepositoryService.RepositoryInfo
import util.Implicits._
import util.ControlUtil._
/** /**
* Allows only oneself and administrators. * Allows only oneself and administrators.
@@ -13,11 +15,12 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = { private def authenticate(action: => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action case Some(x) if(x.isAdmin) => action
case Some(x) if(paths(1) == x.userName) => action case Some(x) if(paths(0) == x.userName) => action
case _ => Unauthorized() case _ => Unauthorized()
}
} }
} }
} }
@@ -32,14 +35,15 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }
@@ -87,15 +91,16 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }
@@ -109,19 +114,20 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
if(!repository.repository.isPrivate){ if(!repository.repository.isPrivate){
action(repository) action(repository)
} else { } else {
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
}
} }
} } getOrElse NotFound()
} getOrElse NotFound() }
} }
} }
} }
@@ -135,16 +141,17 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = { private def authenticate(action: (RepositoryInfo) => Any) = {
{ {
val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") defining(request.paths){ paths =>
getRepository(paths(1), paths(2), baseUrl).map { repository => getRepository(paths(0), paths(1), baseUrl).map { repository =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository) case Some(x) if(!repository.repository.isPrivate) => action(repository)
case Some(x) if(paths(1) == x.userName) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository)
case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
}
} }
} }
} }

View File

@@ -0,0 +1,48 @@
package util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
/**
* Provides control facilities.
*/
object ControlUtil {
def defining[A, B](value: A)(f: A => B): B = f(value)
def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
try f(resource) finally {
if(resource != null){
try {
resource.close()
} catch {
case e: Throwable => // ignore
}
}
}
def using[T](git: Git)(f: Git => T): T =
try f(git) finally git.getRepository.close
def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
try f(git1, git2) finally {
git1.getRepository.close
git2.getRepository.close
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
try f(revWalk) finally revWalk.release()
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
def executeIf(condition: => Boolean)(action: => Unit): Boolean =
if(condition){
action
true
} else false
def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
if(condition) action else None
}

View File

@@ -1,34 +1,38 @@
package util package util
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git import util.ControlUtil._
import org.eclipse.jgit.lib.Ref
/** /**
* Provides directories used by GitBucket. * Provides directories used by GitBucket.
*/ */
object Directory { object Directory {
val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
case Some(env) => new File(env)
case None => new File(System.getProperty("user.home"), "gitbucket")
}).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories" val RepositoryHome = s"${GitBucketHome}/repositories"
val DatabaseHome = s"${GitBucketHome}/data"
/** /**
* Repository names of the specified user. * Repository names of the specified user.
*/ */
def getRepositories(owner: String): List[String] = { def getRepositories(owner: String): List[String] =
val dir = new File(s"${RepositoryHome}/${owner}") defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
if(dir.exists){ if(dir.exists){
dir.listFiles.filter { file => dir.listFiles.filter { file =>
file.isDirectory && !file.getName.endsWith(".wiki.git") file.isDirectory && !file.getName.endsWith(".wiki.git")
}.map(_.getName.replaceFirst("\\.git$", "")).toList }.map(_.getName.replaceFirst("\\.git$", "")).toList
} else { } else {
Nil Nil
}
} }
}
/** /**
* Substance directory of the repository. * Substance directory of the repository.
*/ */

View File

@@ -1,33 +0,0 @@
package util
import java.text.SimpleDateFormat
import javax.servlet.http.HttpSession
import util.Directory._
import org.apache.commons.io.FileUtils
object FileUploadUtil {
def generateFileId: String =
new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
def TemporaryDir(implicit session: HttpSession): java.io.File =
new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
new java.io.File(TemporaryDir, fileId)
// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
// getTemporaryFile(fileId).delete()
def removeTemporaryFiles()(implicit session: HttpSession): Unit =
FileUtils.deleteDirectory(TemporaryDir)
def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
if(filename.isDefined){
session.removeAttribute("upload_" + fileId)
}
filename
}
}

View File

@@ -1,22 +1,31 @@
package util package util
import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils} import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection import java.net.URLConnection
import java.io.File import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream} import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
import util.ControlUtil._
object FileUtil { object FileUtil {
def getMimeType(name: String): String = { def getMimeType(name: String): String =
val fileNameMap = URLConnection.getFileNameMap() defining(URLConnection.getFileNameMap()){ fileNameMap =>
val mimeType = fileNameMap.getContentTypeFor(name) fileNameMap.getContentTypeFor(name) match {
if(mimeType == null){ case null => "application/octet-stream"
"application/octeat-stream" case mimeType => mimeType
} else { }
mimeType }
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 isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000) def isLarge(size: Long): Boolean = (size > 1024 * 1000)
@@ -36,21 +45,29 @@ object FileUtil {
} }
} }
val out = new ZipArchiveOutputStream(dest) using(new ZipArchiveOutputStream(dest)){ out =>
try {
addDirectoryToZip(out, dir, dir.getName) addDirectoryToZip(out, dir, dir.getName)
} finally {
IOUtils.closeQuietly(out)
} }
} }
def getExtension(name: String): String = { def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
val index = name.lastIndexOf('.') if(i >= 0) path.substring(i + 1) else path
if(index >= 0){
name.substring(index + 1)
} else {
""
}
} }
} def getExtension(name: String): String =
name.lastIndexOf('.') match {
case i if(i >= 0) => name.substring(i + 1)
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

@@ -1,7 +1,7 @@
package util package util
import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex import scala.util.matching.Regex
import javax.servlet.http.{HttpSession, HttpServletRequest}
/** /**
* Provides some usable implicit conversions. * Provides some usable implicit conversions.
@@ -13,7 +13,7 @@ object Implicits {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@scala.annotation.tailrec @scala.annotation.tailrec
private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = { private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] =
list match { list match {
case x :: xs => { case x :: xs => {
xs.span(condition(x, _)) match { xs.span(condition(x, _)) match {
@@ -22,12 +22,6 @@ object Implicits {
} }
case Nil => result case Nil => result
} }
}
}
// TODO Should this implicit conversion move to model.Functions?
implicit class RichColumn(c1: Column[Boolean]){
def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
} }
implicit class RichString(value: String){ implicit class RichString(value: String){
@@ -47,6 +41,38 @@ object Implicits {
} }
sb.toString sb.toString
} }
def toIntOpt: Option[Int] = try {
Option(Integer.parseInt(value))
} catch {
case e: NumberFormatException => None
}
} }
} implicit class RichRequest(request: HttpServletRequest){
def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
def hasQueryString: Boolean = request.getQueryString != null
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
}
implicit class RichSession(session: HttpSession){
def putAndGet[T](key: String, value: T): T = {
session.setAttribute(key, value)
value
}
def getAndRemove[T](key: String): Option[T] = {
val value = session.getAttribute(key).asInstanceOf[T]
if(value == null){
session.removeAttribute(key)
}
Option(value)
}
}
}

View File

@@ -2,19 +2,19 @@ package util
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import util.Directory._ import util.Directory._
import util.StringUtil._
import util.ControlUtil._
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import javax.servlet.ServletContext
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._ import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.revwalk.filter._ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff._
import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException import org.eclipse.jgit.api.errors.NoHeadException
import service.RepositoryService
/** /**
* Provides complex JGit operations. * Provides complex JGit operations.
@@ -71,29 +71,17 @@ object JGitUtil {
rev.getFullMessage, rev.getFullMessage,
rev.getParents().map(_.name).toList) rev.getParents().map(_.name).toList)
val summary = { val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n") defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
val firstLine = if(i >= 0){ if(firstLine.length > shortMessage.length) shortMessage else firstLine
fullMessage.trim.substring(0, i).trim
} else {
fullMessage
}
if(firstLine.length > shortMessage.length){
shortMessage
} else {
firstLine
} }
} }
val description = { val description = defining(fullMessage.trim.indexOf("\n")){ i =>
val i = fullMessage.trim.indexOf("\n") optionIf(i >= 0){
if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else {
None
} }
} }
} }
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -116,33 +104,18 @@ object JGitUtil {
case class TagInfo(name: String, time: Date, id: String) case class TagInfo(name: String, time: Date, id: String)
/** /**
* Use this method to use the Git object. * Returns RevCommit from the commit or tag id.
* Repository resources are released certainly after processing.
*/
def withGit[T](dir: java.io.File)(f: Git => T): T = withGit(Git.open(dir))(f)
/**
* Use this method to use the Git object.
* Repository resources are released certainly after processing.
*/
def withGit[T](git: Git)(f: Git => T): T = {
try {
f(git)
} finally {
git.getRepository.close
}
}
/**
* Returns RevCommit from the commit id.
* *
* @param git the Git object * @param git the Git object
* @param commitId the ObjectId of the commit * @param objectId the ObjectId of the commit or tag
* @return the RevCommit for the specified commit * @return the RevCommit for the specified commit or tag
*/ */
def getRevCommitFromId(git: Git, commitId: ObjectId): RevCommit = { def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
val revWalk = new RevWalk(git.getRepository) val revWalk = new RevWalk(git.getRepository)
val revCommit = revWalk.parseCommit(commitId) val revCommit = revWalk.parseAny(objectId) match {
case r: RevTag => revWalk.parseCommit(r.getObject)
case _ => revWalk.parseCommit(objectId)
}
revWalk.dispose revWalk.dispose
revCommit revCommit
} }
@@ -151,15 +124,10 @@ object JGitUtil {
* Returns the repository information. It contains branch names and tag names. * Returns the repository information. It contains branch names and tag names.
*/ */
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
withGit(getRepositoryDir(owner, repository)){ git => using(Git.open(getRepositoryDir(owner, repository))){ git =>
try { try {
// get commit count // get commit count
val i = git.log.all.call.iterator val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
var commitCount = 0
while(i.hasNext && commitCount <= 1000){
i.next
commitCount = commitCount + 1
}
RepositoryInfo( RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -193,45 +161,43 @@ object JGitUtil {
* @return HTML of the file list * @return HTML of the file list
*/ */
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
while (treeWalk.next()) { using(new RevWalk(git.getRepository)){ revWalk =>
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) val objectId = git.getRepository.resolve(revision)
val revCommit = revWalk.parseCommit(objectId)
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
if(path != "."){
treeWalk.setRecursive(true)
treeWalk.setFilter(new TreeFilter(){
var stopRecursive = false
def include(walker: TreeWalk): Boolean = {
val targetPath = walker.getPathString
if((path + "/").startsWith(targetPath)){
true
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
stopRecursive = true
treeWalk.setRecursive(false)
true
} else {
false
}
}
def shouldBeRecursive(): Boolean = !stopRecursive
override def clone: TreeFilter = return this
})
}
while (treeWalk.next()) {
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
}
}
} }
treeWalk.release
revWalk.dispose
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) => list.map { case (objectId, fileMode, path, name) =>
@@ -261,7 +227,7 @@ object JGitUtil {
* @param page the page number (1-) * @param page the page number (1-)
* @param limit the number of commit info per page. 0 (default) means unlimited. * @param limit the number of commit info per page. 0 (default) means unlimited.
* @param path filters by this path. default is no filter. * @param path filters by this path. default is no filter.
* @return a tuple of the commit list and whether has next * @return a tuple of the commit list and whether has next, or the error message
*/ */
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = { def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if(page <= 0) 1 else page val fixedPage = if(page <= 0) 1 else page
@@ -276,27 +242,48 @@ object JGitUtil {
case _ => (logs, i.hasNext) case _ => (logs, i.hasNext)
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision) defining(git.getRepository.resolve(revision)){ objectId =>
if(objectId == null){ if(objectId == null){
Left(s"${revision} can't be resolved.") Left(s"${revision} can't be resolved.")
} else { } else {
revWalk.markStart(revWalk.parseCommit(objectId)) revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){ if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){ revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = { def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
} }
override def clone(): RevFilter = this Right(getCommitLog(revWalk.iterator, 0, Nil))
}) }
} }
val commits = getCommitLog(revWalk.iterator, 0, Nil)
revWalk.release
Right(commits)
} }
} }
def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)
(endCondition: RevCommit => Boolean): List[CommitInfo] = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
i.hasNext match {
case true => {
val revCommit = i.next
if(endCondition(revCommit)){
if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs
} else {
getCommitLog(i, logs :+ new CommitInfo(revCommit))
}
}
case false => logs
}
using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
getCommitLog(revWalk.iterator, Nil).reverse
}
}
/** /**
* Returns the commit list between two revisions. * Returns the commit list between two revisions.
@@ -306,30 +293,9 @@ object JGitUtil {
* @param to the to revision * @param to the to revision
* @return the commit list * @return the commit list
*/ */
def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = { // TODO swap parameters 'from' and 'to'!?
@scala.annotation.tailrec def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] =
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] = getCommitLogs(git, to)(_.getName == from)
i.hasNext match {
case true => {
val revCommit = i.next
if(revCommit.name == from){
logs
} else {
getCommitLog(i, logs :+ new CommitInfo(revCommit))
}
}
case false => logs
}
val revWalk = new RevWalk(git.getRepository)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(to)))
val commits = getCommitLog(revWalk.iterator, Nil)
revWalk.release
commits.reverse
}
/** /**
* Returns the latest RevCommit of the specified path. * Returns the latest RevCommit of the specified path.
@@ -351,51 +317,11 @@ object JGitUtil {
* @return the list of latest commit * @return the list of latest commit
*/ */
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = { def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
val map = new scala.collection.mutable.HashMap[String, RevCommit] paths.map { path =>
val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
val revWalk = new RevWalk(git.getRepository) (path, commit)
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision))) }.toMap
//revWalk.sort(RevSort.REVERSE);
val i = revWalk.iterator
while(i.hasNext && map.size != paths.length){
val commit = i.next
if(commit.getParentCount == 0){
// Initial commit
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.reset()
treeWalk.setRecursive(true)
treeWalk.addTree(commit.getTree)
while (treeWalk.next) {
paths.foreach { path =>
if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
treeWalk.release
} else {
(0 to commit.getParentCount - 1).foreach { i =>
val parent = revWalk.parseCommit(commit.getParent(i).getId())
val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
df.setRepository(git.getRepository)
df.setDiffComparator(RawTextComparator.DEFAULT)
df.setDetectRenames(true)
val diffs = df.scan(parent.getTree(), commit.getTree)
diffs.asScala.foreach { diff =>
paths.foreach { path =>
if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
map.put(path, commit)
}
}
}
}
}
revWalk.release
}
map.toMap
} }
/** /**
@@ -411,126 +337,131 @@ object JGitUtil {
if(large == false && FileUtil.isLarge(loader.getSize)){ if(large == false && FileUtil.isLarge(loader.getSize)){
None None
} else { } else {
val db = git.getRepository.getObjectDatabase using(git.getRepository.getObjectDatabase){ db =>
try {
Some(db.open(id).getBytes) Some(db.open(id).getBytes)
} finally {
db.close
} }
} }
} catch { } catch {
case e: MissingObjectException => None case e: MissingObjectException => None
} }
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): List[DiffInfo] = { /**
* Returns the tuple of diff of the given commit and the previous commit id.
*/
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec @scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match { i.hasNext match {
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next) case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
case _ => logs case _ => logs
} }
val revWalk = new RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
val commits = getCommitLog(revWalk.iterator, Nil)
val commits = getCommitLog(revWalk.iterator, Nil) val revCommit = commits(0)
revWalk.release
if(commits.length >= 2){
val revCommit = commits(0) // not initial commit
val oldCommit = commits(1)
if(commits.length >= 2){ (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
// not initial commit
val oldCommit = commits(1) } else {
// initial commit
// get diff between specified commit and its previous commit using(new TreeWalk(git.getRepository)){ treeWalk =>
val reader = git.getRepository.newObjectReader treeWalk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
val oldTreeIter = new CanonicalTreeParser while(treeWalk.next){
oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}")) buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
val newTreeIter = new CanonicalTreeParser } else {
newTreeIter.reset(reader, git.getRepository.resolve(id + "^{tree}")) DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
import scala.collection.JavaConverters._ }))
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => }
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){ (buffer.toList, None)
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
} }
}.toList
} else {
// initial commit
val walk = new TreeWalk(git.getRepository)
walk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(walk.next){
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None)
} else {
DiffInfo(ChangeType.ADD, null, walk.getPathString, None,
JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
}))
} }
walk.release
buffer.toList
} }
} }
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
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._
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
}
}.toList
}
/** /**
* Returns the list of branch names of the specified commit. * Returns the list of branch names of the specified commit.
*/ */
def getBranchesOfCommit(git: Git, commitId: String): List[String] = { def getBranchesOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
}.map { e => }.toList.sorted
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) }
}.toList.sorted
} finally {
walk.release
} }
}
/** /**
* Returns the list of tags of the specified commit. * Returns the list of tags of the specified commit.
*/ */
def getTagsOfCommit(git: Git, commitId: String): List[String] = { def getTagsOfCommit(git: Git, commitId: String): List[String] =
val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) using(new RevWalk(git.getRepository)){ revWalk =>
try { defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
(e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
git.getRepository.getAllRefs.entrySet.asScala.filter { e => }.map { e =>
(e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
}.map { e => }.toList.sorted.reverse
e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) }
}.toList.sorted.reverse
} finally {
walk.release
} }
}
def initRepository(dir: java.io.File): Unit = { def initRepository(dir: java.io.File): Unit =
val repository = new RepositoryBuilder().setGitDir(dir).setBare.build using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
try {
repository.create repository.create
setReceivePack(repository) setReceivePack(repository)
} finally {
repository.close
} }
def cloneRepository(from: java.io.File, to: java.io.File): Unit =
using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
setReceivePack(git.getRepository)
}
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
defining(repository.getConfig){ config =>
config.setBoolean("http", null, "receivepack", true)
config.save
}
def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
revstr: String = ""): Option[(ObjectId, String)] = {
Seq(
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)
} }
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = { }
val config = repository.getConfig
config.setBoolean("http", null, "receivepack", true)
config.save
}
}

View File

@@ -0,0 +1,72 @@
package util
/**
* Define key strings for request attributes, session attributes or flash attributes.
*/
object Keys {
/**
* Define session keys.
*/
object Session {
/**
* Session key for the logged in account information.
*/
val LoginAccount = "LOGIN_ACCOUNT"
/**
* Session key for the redirect URL.
*/
val Redirect = "REDIRECT"
/**
* Session key for the issue search condition in dashboard.
*/
val DashboardIssues = "dashboard/issues"
/**
* Session key for the pull request search condition in dashboard.
*/
val DashboardPulls = "dashboard/pulls"
/**
* Generate session key for the issue search condition.
*/
def Issues(owner: String, name: String) = s"${owner}/${name}/issues"
/**
* Generate session key for the pull request search condition.
*/
def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls"
/**
* Generate session key for the upload filename.
*/
def Upload(fileId: String) = s"upload_${fileId}"
}
/**
* Define request keys.
*/
object Request {
/**
* Request key for the Ajax request flag.
*/
val Ajax = "AJAX"
/**
* Request key for the username which is used during Git repository access.
*/
val UserName = "USER_NAME"
/**
* Generate request key for the request cache.
*/
def Cache(key: String) = s"cache.${key}"
}
}

View File

@@ -0,0 +1,104 @@
package util
import util.ControlUtil._
import service.SystemSettingsService
import com.novell.ldap._
import service.SystemSettingsService.Ldap
import scala.annotation.tailrec
/**
* Utility for LDAP authentication.
*/
object LDAPUtil {
private val LDAP_VERSION: Int = 3
/**
* Try authentication by LDAP using given configuration.
* Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
*/
def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
bind(
ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
ldapSettings.bindDN.getOrElse(""),
ldapSettings.bindPassword.getOrElse("")
) match {
case Some(conn) => {
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("System LDAP authentication failed.")
}
}
private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
bind(
ldapSettings.host,
ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
userDN,
password
) match {
case Some(conn) => {
withConnection(conn) { conn =>
findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
case Some(mailAddress) => Right(mailAddress)
case None => Left("Can't find mail address.")
}
}
}
case None => Left("User LDAP Authentication Failed.")
}
}
private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
val conn: LDAPConnection = new LDAPConnection
try {
conn.connect(host, port)
conn.bind(LDAP_VERSION, dn, password.getBytes)
Some(conn)
} catch {
case e: Exception => {
if (conn.isConnected) conn.disconnect()
None
}
}
}
private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
try {
f(conn)
} finally {
conn.disconnect()
}
}
private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
@tailrec
def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
if(results.hasMore){
getEntries(results, entries :+ (try {
Option(results.next)
} catch {
case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
}))
} else {
entries.flatten
}
}
getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
case x => x.getDN
}
}
private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
optionIf (results.hasMore) {
Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
}
}
}

View File

@@ -0,0 +1,36 @@
package util
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.{ReentrantLock, Lock}
import util.ControlUtil._
object LockUtil {
/**
* lock objects
*/
private val locks = new ConcurrentHashMap[String, Lock]()
/**
* Returns the lock object for the specified repository.
*/
private def getLockObject(key: String): Lock = synchronized {
if(!locks.containsKey(key)){
locks.put(key, new ReentrantLock())
}
locks.get(key)
}
/**
* Synchronizes a given function which modifies the working copy of the wiki repository.
*/
def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
try {
lock.lock()
f
} finally {
lock.unlock()
}
}
}

View File

@@ -0,0 +1,111 @@
package util
import scala.concurrent._
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import app.Context
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
import servlet.Database
import SystemSettingsService.Smtp
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
(
// individual repository's owner
issue.userName ::
// collaborators
getCollaborators(issue.userName, issue.repositoryName) :::
// participants
issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
)
.distinct
.withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
.foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
}
object Notifier {
// TODO We want to be able to switch to mock.
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if settings.notification => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
def msgIssue(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgPullRequest(url: String) = (content: String) => s"""
|${content}<hr/>
|View, comment on, or merge it at:<br/>
|<a href="${url}">${url}</a>
""".stripMargin
def msgComment(url: String) = (content: String) => s"""
|${content}<br/>
|--<br/>
|<a href="${url}">View it on GitBucket</a>
""".stripMargin
def msgStatus(url: String) = (content: String) => s"""
|${content} <a href="${url}">#${url split('/') last}</a>
""".stripMargin
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context) = {
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)
}
smtp.fromAddress
.map (_ -> smtp.fromName.orNull)
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
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
}
}
}
"Notifications Successful."
}
f onSuccess {
case s => logger.debug(s)
}
f onFailure {
case t => logger.error("Notifications Failed.", t)
}
}
}
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}

View File

@@ -1,14 +1,16 @@
package util package util
import java.net.{URLDecoder, URLEncoder} import java.net.{URLDecoder, URLEncoder}
import org.mozilla.universalchardet.UniversalDetector
import util.ControlUtil._
object StringUtil { object StringUtil {
def sha1(value: String): String = { def sha1(value: String): String =
val md = java.security.MessageDigest.getInstance("SHA-1") defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
md.update(value.getBytes) md.update(value.getBytes)
md.digest.map(b => "%02x".format(b)).mkString md.digest.map(b => "%02x".format(b)).mkString
} }
def md5(value: String): String = { def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5") val md = java.security.MessageDigest.getInstance("MD5")
@@ -20,4 +22,20 @@ object StringUtil {
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8") def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
def detectEncoding(content: Array[Byte]): String =
defining(new UniversalDetector(null)){ detector =>
detector.handleData(content, 0, content.length)
detector.dataEnd()
detector.getDetectedCharset match {
case null => "UTF-8"
case e => e
}
}
} }

View File

@@ -1,7 +1,6 @@
package util package util
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import scala.Some
trait Validations { trait Validations {
@@ -9,7 +8,7 @@ trait Validations {
* Constraint for the identifier such as user name, repository name or page name. * Constraint for the identifier such as user name, repository name or page name.
*/ */
def identifier: Constraint = new Constraint(){ def identifier: Constraint = new Constraint(){
def validate(name: String, value: String): Option[String] = 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.") Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){ } else if(value.startsWith("_") || value.startsWith("-")){
@@ -26,10 +25,7 @@ trait Validations {
*/ */
def date(constraints: Constraint*): SingleValueType[java.util.Date] = def date(constraints: Constraint*): SingleValueType[java.util.Date] =
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){ new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
def convert(value: String): java.util.Date = { def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd")
formatter.parse(value)
}
} }
} }

View File

@@ -12,17 +12,23 @@ trait AvatarImageProvider { self: RequestCache =>
*/ */
protected def getAvatarImageHtml(userName: String, size: Int, protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = { mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}""" 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"""
}
} getOrElse { } getOrElse {
if(mailAddress.nonEmpty){ if(mailAddress.nonEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}""" s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
} else { } else {
s"""${context.path}/${userName}/_avatar""" s"""${context.path}/${userName}/_avatar"""
} }
} }
if(tooltip){ if(tooltip){
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title=${userName}/>""") Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""")
} else { } else {
Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""") Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""")
} }

View File

@@ -1,12 +1,14 @@
package view package view
import util.StringUtil import util.StringUtil
import util.ControlUtil._
import util.Directory._
import org.parboiled.common.StringUtils import org.parboiled.common.StringUtils
import org.pegdown._ import org.pegdown._
import org.pegdown.ast._ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.RequestCache import service.{RequestCache, WikiService}
object Markdown { object Markdown {
@@ -29,7 +31,7 @@ object Markdown {
} }
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo, 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 = { override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){ if(enableWikiLink){
try { try {
@@ -40,8 +42,14 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} else { } else {
(text, text) (text, text)
} }
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) 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 { } catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
} }
@@ -52,7 +60,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
} }
class GitBucketVerbatimSerializer extends VerbatimSerializer { class GitBucketVerbatimSerializer extends VerbatimSerializer {
def serialize(node: VerbatimNode, printer: Printer) { def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print("<pre") printer.println.print("<pre")
if (!StringUtils.isEmpty(node.getType)) { if (!StringUtils.isEmpty(node.getType)) {
printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"') printer.print(" class=").print('"').print("prettyprint ").print(node.getType).print('"')
@@ -98,11 +106,11 @@ class GitBucketHtmlSerializer(
} }
} }
private def printAttribute(name: String, value: String) { private def printAttribute(name: String, value: String): Unit = {
printer.print(' ').print(name).print('=').print('"').print(value).print('"') printer.print(' ').print(name).print('=').print('"').print(value).print('"')
} }
override def visit(node: TextNode) { override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // convert commit id and username to link.
val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText

View File

@@ -9,12 +9,12 @@ import service.RequestCache
* Provides helper methods for Twirl templates. * Provides helper methods for Twirl templates.
*/ */
object helpers extends AvatarImageProvider with LinkConverter with RequestCache { object helpers extends AvatarImageProvider with LinkConverter with RequestCache {
/** /**
* Format java.util.Date to "yyyy-MM-dd HH:mm:ss". * 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) def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
/** /**
* Format java.util.Date to "yyyy-MM-dd". * Format java.util.Date to "yyyy-MM-dd".
*/ */
@@ -31,9 +31,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink))
}
/** /**
* Returns &lt;img&gt; which displays the avatar icon. * Returns &lt;img&gt; which displays the avatar icon.
@@ -51,15 +50,36 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html = def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(convertRefsLinks(value, repository)) Html(convertRefsLinks(value, repository))
def cut(value: String, length: Int): String =
if(value.length > length){
value.substring(0, length) + "..."
} else {
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 = def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""") .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("\\[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("\\[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]+?)\\]" , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$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>""") .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: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("") def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
@@ -73,14 +93,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
/** /**
* Generates the url to the account page. * Generates the url to the account page.
*/ */
def url(userName: String)(implicit context: app.Context): String = def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
s"${context.path}/${userName}"
/** /**
* Returns the url to the root of assets. * Returns the url to the root of assets.
*/ */
def assets(implicit context: app.Context): String = def assets(implicit context: app.Context): String = s"${context.path}/assets"
s"${context.path}/assets"
/** /**
* Generates the link to the account page. * Generates the link to the account page.
@@ -91,6 +109,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
} getOrElse Html(userName) } getOrElse Html(userName)
} }
def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
/** /**
* Implicit conversion to add mkHtml() to Seq[Html]. * Implicit conversion to add mkHtml() to Seq[Html].
*/ */

View File

@@ -1,23 +1,6 @@
@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context) @(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(account.userName){ @main(account, groupNames, "activity"){
<div class="container-fluid"> @helper.html.activities(activities)
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="block-header">@account.userName</div>
</div>
<div class="block">
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "activity")
@helper.html.activities(activities)
</div>
</div>
</div>
} }

View File

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

View File

@@ -0,0 +1,49 @@
@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(account.userName){
<div class="container-fluid">
<div class="row-fluid">
<div class="span4">
<div class="block">
<div class="account-image">@avatar(account.userName, 200)</div>
<div class="account-fullname">@account.fullName</div>
<div class="account-username">@account.userName</div>
</div>
<div class="block">
@if(account.url.isDefined){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div>
}
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
@if(groupNames.nonEmpty){
<div>
<div>Groups</div>
@groupNames.map { groupName =>
<a href="@url(groupName)">@avatar(groupName, 36, tooltip = true)</a>
}
</div>
}
</div>
<div class="span8">
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
} else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
}
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>
@body
</div>
</div>
</div>
}

View File

@@ -0,0 +1,16 @@
@(account: model.Account, members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@main(account, Nil, "members"){
@if(members.isEmpty){
No members
} else {
@members.map { userName =>
<div class="block">
<div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a>
</div>
</div>
}
}
}

View File

@@ -1,42 +1,26 @@
@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) @(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(account.userName){ @main(account, groupNames, "repositories"){
<div class="container-fluid"> @if(repositories.isEmpty){
<div class="row-fluid"> No repositories
<div class="span4"> } else {
<div class="block"> @repositories.map { repository =>
<div class="account-image">@avatar(account.userName, 200)</div> <div class="block">
<div class="block-header">@account.userName</div> <div class="block-header">
</div> <a href="@url(repository)">@repository.name</a>
<div class="block"> @if(repository.repository.isPrivate){
<div><i class="icon-home"></i> <a href="@account.url">@account.url</a></div> <i class="icon-lock"></i>
<div><i class="icon-time"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
</div>
</div>
<div class="span8">
@tab(account, "repositories")
@if(repositories.isEmpty){
No repositories
} else {
@repositories.map { repository =>
<div class="block">
<div class="block-header">
<a href="@url(repository.owner)">@repository.owner</a>
/
<a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){
<i class="icon-lock"></i>
}
</div>
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div>
} }
</div>
@if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
} }
@if(repository.repository.description.isDefined){
<div>@repository.repository.description</div>
}
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
</div> </div>
</div> }
</div> }
} }

View File

@@ -1,14 +0,0 @@
@(account: model.Account, active: String)(implicit context: app.Context)
@import context._
@import view.helpers._
<ul class="nav nav-tabs">
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
<li class="pull-right">
<div class="button-group">
<a href="@url(account.userName)/_edit" class="btn">Edit Your Profile</a>
</div>
</li>
}
</ul>

View File

@@ -10,6 +10,9 @@
<li@if(active=="system"){ class="active"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@path/admin/system">System Settings</a>
</li> </li>
<li>
<a href="@path/console/login.jsp">H2 Console</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -8,17 +8,151 @@
<div class="box"> <div class="box">
<div class="box-header">System Settings</div> <div class="box-header">System Settings</div>
<div class="box-content"> <div class="box-content">
<label><strong>Account registration</strong></label> <!--====================================================================-->
<!-- Account registration -->
<!--====================================================================-->
<label class="strong">Account registration</label>
<fieldset> <fieldset>
<label> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}>
<strong>Allow</strong> - Users can create account by themselves. <span class="strong">Allow</span> - Users can create accounts by themselves.
</label> </label>
<label> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}>
<strong>Deny</strong> - Only administrators can create account. <span class="strong">Deny</span> - Only administrators can create accounts.
</label> </label>
</fieldset> </fieldset>
<!--====================================================================-->
<!-- Services -->
<!--====================================================================-->
<hr>
<label class="strong">Services</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/>
Use Gravatar for Profile-Images
</label>
</fieldset>
<!--====================================================================-->
<!-- Authentication -->
<!--====================================================================-->
<hr>
<label class="strong">Authentication</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked}/>
LDAP
</label>
</fieldset>
<div class="form-horizontal ldap">
<div class="control-group">
<label class="control-label" for="ldapHost">LDAP Host</label>
<div class="controls">
<input type="text" id="ldapHost" name="ldap.host" value="@settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapPort">LDAP Port</label>
<div class="controls">
<input type="text" id="ldapPort" name="ldap.port" class="input-mini" value="@settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindDN">Bind DN</label>
<div class="controls">
<input type="text" id="ldapBindDN" name="ldap.bindDN" value="@settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBindPassword">Bind Password</label>
<div class="controls">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" value="@settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapBaseDN">Base DN</label>
<div class="controls">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" value="@settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapUserNameAttribute">User name attribute</label>
<div class="controls">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" value="@settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="ldapMailAttribute">Mail address attribute</label>
<div class="controls">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" value="@settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Notification email -->
<!--====================================================================-->
<hr>
<label class="strong">Notification email</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/>
Send notifications
</label>
</fieldset>
<div class="form-horizontal notification">
<div class="control-group">
<label class="control-label" for="smtpHost">SMTP Host</label>
<div class="controls">
<input type="text" id="smtpHost" name="smtp.host" value="@settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPort">SMTP Port</label>
<div class="controls">
<input type="text" id="smtpPort" name="smtp.port" class="input-mini" value="@settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpUser">SMTP User</label>
<div class="controls">
<input type="text" id="smtpUser" name="smtp.user" value="@settings.smtp.map(_.user)"/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="smtpPassword">SMTP Password</label>
<div class="controls">
<input type="password" id="smtpPassword" name="smtp.password" value="@settings.smtp.map(_.password)"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
</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>
</div> </div>
<fieldset> <fieldset>
@@ -26,4 +160,15 @@
</fieldset> </fieldset>
</form> </form>
} }
} }
<script>
$(function(){
$('#notification').change(function(){
$('.notification input').prop('disabled', !$(this).prop('checked'));
}).change();
$('#ldapAuthentication').change(function(){
$('.ldap input').prop('disabled', !$(this).prop('checked'));
}).change();
});
</script>

View File

@@ -1,55 +0,0 @@
@(account: Option[model.Account])(implicit context: app.Context)
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_new} else {@path/admin/users/@account.get.userName/_edit}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<label for="userName"><strong>Username</strong></label>
<input type="text" name="userName" id="userName" value="@account.map(_.userName)"@if(account.isDefined){ readonly}/>
<span id="error-userName" class="error"></span>
</fieldset>
<fieldset>
<label for="password"><strong>Password</strong>
@if(account.isDefined){
(Input to change password)
}
</label>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
<span id="error-password" class="error"></span>
</fieldset>
<fieldset>
<label for="mailAddress"><strong>Mail Address</strong></label>
<input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<fieldset>
<label><strong>User Type</strong></label>
<label 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">
<input type="radio" name="isAdmin" id="userType_Admin" value="true"@if(account.isDefined && account.get.isAdmin){ checked}/> Administrator
</label>
</fieldset>
<fieldset>
<label><strong>URL (Optional)</strong></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"><strong>Image (Optional)</strong></label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}

View File

@@ -0,0 +1,116 @@
@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(if(account.isEmpty) "New Group" else "Update Group"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
<div class="row-fluid">
<div class="span7">
<fieldset>
<label for="groupName" class="strong">Group name</label>
<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>
<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>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
<div class="span5">
<fieldset>
<label class="strong">Members</label>
<ul id="members" class="collaborator">
@members.map { userName =>
<li data-name="@userName">
<a href="@path/@url(userName)">@userName</a>
<a href="#" class="remove">(remove)</a>
</li>
}
</ul>
@helper.html.account("memberName", 200)
<input type="button" class="btn" value="Add" id="addMember"/>
<input type="hidden" id="memberNames" name="memberNames" value="@members.mkString(",")"/>
<div>
<span class="error" id="error-memberName"></span>
</div>
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}
<script>
$(function(){
$('#addMember').click(function(){
$('#error-memberName').text('');
var userName = $('#memberName').val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check duplication
var exists = $('#members li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-memberName').text('User has been already added.');
return false;
}
// check existence
$.post('@path/admin/users/_usercheck', {
'userName': userName
}, function(data, status){
if(data == 'true'){
// add member
$('#members').append($('<li>')
.data('name', userName)
.append($('<a>').attr('href', '@path/' + userName).text(userName))
.append(' ')
.append($('<a>').attr('href', '#').addClass('remove').text('(remove)')));
$('#memberName').val('');
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
} else {
$('#error-memberName').text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
// remove member
$(this).parent().remove();
// update hidden value
var userNames = $('#members li').map(function(i, e){
return $(e).data('name');
}).get().join(',');
$('#memberNames').val(userNames);
});
// Don't submit form by ENTER key
$('#memberName').keypress(function(e){
console.log(e.keyCode);
return !(e.keyCode == 13);
});
});
</script>

View File

@@ -1,30 +1,46 @@
@(users: List[model.Account])(implicit context: app.Context) @(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Manage Users"){ @html.main("Manage Users"){
@admin.html.menu("users"){ @admin.html.menu("users"){
<div style="text-align: right; margin-bottom: 4px;"> <div style="text-align: right; margin-bottom: 4px;">
<a href="@path/admin/users/_new" class="btn">New User</a> <a href="@path/admin/users/_newuser" class="btn">New User</a>
<a href="@path/admin/users/_newgroup" class="btn">New Group</a>
</div> </div>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
@users.map { account => @users.map { account =>
<tr> <tr>
<td> <td>
<div class="pull-right"> <div class="pull-right">
<a href="@path/admin/users/@account.userName/_edit">Edit</a> @if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a>
} else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a>
}
</div> </div>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20) @avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@url(account.userName)">@account.userName</a>
@if(account.isAdmin){ @if(account.isGroupAccount){
(Administrator) (Group)
} else { } else {
(Normal) @if(account.isAdmin){
(Administrator)
} else {
(Normal)
}
}
@if(account.isGroupAccount){
@members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true)
}
} }
</div> </div>
<div> <div>
<hr> <hr>
<i class="icon-envelope"></i> @account.mailAddress @if(!account.isGroupAccount){
<i class="icon-envelope"></i> @account.mailAddress
}
@account.url.map { url => @account.url.map { url =>
<i class="icon-home"></i> @url <i class="icon-home"></i> @url
} }
@@ -32,7 +48,9 @@
<div> <div>
<span class="muted">Registered:</span> @datetime(account.registeredDate) <span class="muted">Registered:</span> @datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate) <span class="muted">Updated:</span> @datetime(account.updatedDate)
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime) @if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime)
}
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,74 @@
@(account: Option[model.Account])(implicit context: app.Context)
@import context._
@html.main(if(account.isEmpty) "New User" else "Update User"){
@admin.html.menu("users"){
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
<div class="row-fluid">
<div class="span6">
<fieldset>
<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)){
<fieldset>
<label for="password" class="strong">
Password
@if(account.isDefined){
(Input to change password)
}
:
</label>
<div>
<span id="error-password" class="error"></span>
</div>
<input type="password" name="password" id="password" value="" autocomplete="off"/>
</fieldset>
}
<fieldset>
<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 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 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>
<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>
<div class="span6">
<fieldset>
<label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account)
</fieldset>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn">Cancel</a>
</fieldset>
</form>
}
}

View File

@@ -0,0 +1,48 @@
@(listparts: twirl.api.Html,
allCount: Int,
assignedCount: Int,
createdByCount: Int,
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
@dashboard.html.tab("issues")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@path/dashboard/issues/repos@condition.toURL">
<span class="count-right">@allCount</span>
In your repositories
</a>
</li>
<li@if(filter == "assigned"){ class="active"}>
<a href="@path/dashboard/issues/assigned@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/issues/created_by@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div>
}

View File

@@ -0,0 +1,40 @@
@(listparts: twirl.api.Html,
counts: List[service.PullRequestService.PullRequestCount],
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Your Issues"){
@dashboard.html.tab("pulls")
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "created_by"){ class="active"}>
<a href="@path/dashboard/pulls/owned@condition.toURL">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
<li@if(filter == "not_created_by"){ class="active"}>
<a href="@path/dashboard/pulls/public@condition.toURL">
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
Public
</a>
</li>
</ul>
<hr/>
<ul class="nav nav-pills nav-stacked small">
@repositories.map { case (owner, name, count) =>
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
<a href="@path/dashboard/pulls/for/@owner/@name">
<span class="count-right">@count</span>
@owner/@name
</a>
</li>
}
</ul>
</div>
@listparts
</div>
}

View File

@@ -0,0 +1,9 @@
@(active: String = "")(implicit context: app.Context)
@import context._
<ul class="nav nav-tabs">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
@if(loginAccount.isDefined){
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
}
</ul>

View File

@@ -1,10 +1,30 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@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 class="head"> <div class="head">
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){ @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">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
} }
</div> </div>
<table class="global-nav box-header"> <table class="global-nav box-header">
@@ -14,16 +34,28 @@
</th> </th>
<th class="box-header@if(active=="issues"){ active}"> <th class="box-header@if(active=="issues"){ active}">
<a href="@url(repository)/issues">Issues</a> <a href="@url(repository)/issues">Issues</a>
@if(repository.issueCount > 0){
<span class="badge">@repository.issueCount</span>
}
</th>
<th class="box-header@if(active=="pulls"){ active}">
<a href="@url(repository)/pulls">Pull Requests</a>
@if(repository.pullCount > 0){
<span class="badge">@repository.pullCount</span>
}
</th> </th>
<th class="box-header@if(active=="wiki"){ active}"> <th class="box-header@if(active=="wiki"){ active}">
<a href="@url(repository)/wiki">Wiki</a> <a href="@url(repository)/wiki">Wiki</a>
</th> </th>
<th class="box-header@if(active=="network"){ active}">
<a href="@url(repository)/network/members">Network</a>
</th>
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){ @if(loginAccount.isDefined && (loginAccount.get.isAdmin || loginAccount.get.userName == repository.owner)){
<th class="box-header@if(active=="settings"){ active}"> <th class="box-header@if(active=="settings"){ active}">
<a href="@url(repository)/settings">Settings</a> <a href="@url(repository)/settings">Settings</a>
</th> </th>
} }
</tr> </tr>
</table> </table>
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){

View File

@@ -0,0 +1,15 @@
@(id: String, width: Int)(implicit context: app.Context)
@import context._
<input type="text" name="@id" id="@id" style="width: @{width}px; margin-bottom: 0px;"/>
<script>
$(function(){
$('#@id').typeahead({
source: function (query, process) {
return $.get('@path/_user/proposals', { query: query },
function (data) {
return process(data.options);
});
}
});
});
</script>

View File

@@ -1,43 +1,96 @@
@(activities: List[model.Activity])(implicit context: app.Context) @(activities: List[model.Activity])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(activities.isEmpty){ @if(activities.isEmpty){
No activity No activity
} else { } else {
@activities.map { activity => @activities.map { activity =>
<div class="block"> <div class="block">
<div class="muted small">@datetime(activity.activityDate)</div> @(activity.activityType match {
<div class="strong"> case "open_issue" => detailActivity(activity, "activity-issue.png")
@avatar(activity.activityUserName, 16) case "comment_issue" => detailActivity(activity, "activity-comment.png")
@activityMessage(activity.message) case "close_issue" => detailActivity(activity, "activity-issue-close.png")
</div> case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
@activity.additionalInfo.map { additionalInfo => case "open_pullreq" => detailActivity(activity, "activity-merge.png")
@(activity.activityType match { case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
case "create_wiki" => { case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
<div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div> case "create_branch" => simpleActivity(activity, "activity-branch.png")
} case "create_tag" => simpleActivity(activity, "activity-tag.png")
case "edit_wiki" => { case "fork" => simpleActivity(activity, "activity-fork.png")
<div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div> case "push" => customActivity(activity, "activity-commit.png"){
} <div class="small activity-message">
case "push" => { {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
<div class="small activity-message"> if(i == 3){
{additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => <div>...</div>
if(i == 3){ } else {
<div>...</div> if(commit.nonEmpty){
} else {
<div> <div>
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit.substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a> <a href={s"${path}/${activity.userName}/${activity.repositoryName}/commit/${commit. substring(0, 40)}"} class="monospace">{commit.substring(0, 7)}</a>
<span>{commit.substring(41)}</span> <span>{commit.substring(41)}</span>
</div> </div>
} }
}} }
</div> }}
</div>
}
case "create_wiki" => customActivity(activity, "activity-wiki.png"){
<div class="small activity-message">
Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${activity.additionalInfo.get}"}>{activity.additionalInfo.get}</a>.
</div>
}
case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
activity.additionalInfo.get.split(":") match {
case Array(pageName, commitId) =>
<div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
<a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}/_compare/${commitId.substring(0, 7)}^...${commitId.substring(0, 7)}"}>View the diff »</a>
</div>
case Array(pageName) =>
<div class="small activity-message">
Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${pageName}"}>{pageName}</a>.
</div>
} }
case _ => { }
<div class=" activity-message">{additionalInfo}</div> })
}
})
}
</div> </div>
} }
} }
@detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@activity.additionalInfo.map { additionalInfo =>
<div class=" activity-message">@additionalInfo</div>
}
</div>
}
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div class="muted small">@datetime(activity.activityDate)</div>
<div class="strong">
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
</div>
@additionalInfo
</div>
}
@simpleActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-small"><img src="@assets/common/images/@image"/></div>
<div class="activity-content">
<div>
@avatar(activity.activityUserName, 16)
@activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span>
</div>
</div>
}

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