mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-31 18:46:28 +01:00 
			
		
		
		
	Merge branch 'master' into #33_match-by-email
Conflicts: src/main/scala/view/helpers.scala
This commit is contained in:
		
							
								
								
									
										82
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								README.md
									
									
									
									
									
								
							| @@ -6,20 +6,25 @@ GitBucket is the easily installable Github clone written with Scala. | ||||
| The current version of GitBucket provides a basic features below: | ||||
|  | ||||
| - 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 | ||||
| - Issues | ||||
| - Fork / Pull request | ||||
| - Mail notification | ||||
| - Activity timeline | ||||
| - 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! | ||||
|  | ||||
| - Fork and pull request | ||||
| - Search | ||||
| - File editing in repository viewer | ||||
| - Comment for the changeset | ||||
| - Network graph | ||||
| - Statics | ||||
| - 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). | ||||
|  | ||||
| @@ -32,29 +37,64 @@ Installation | ||||
|  | ||||
| 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 | ||||
| -------- | ||||
| ### 1.3 - xx Jul 2013 | ||||
| - Batch updating for issues. | ||||
| - Display assigned user on issue list. | ||||
| - User icon and Gravatar support. | ||||
| - Convert @xxxx to link to the account page. | ||||
| - Add copy to clipboard button for git clone URL. | ||||
| - Allows multi-byte characters as wiki page name. | ||||
| - Allows to create the empty repository. | ||||
| - Fixed some bugs. | ||||
| ### 1.6 - 1 Oct 2013 | ||||
| - Web hook | ||||
| - Performance improvement for pull request | ||||
| - Executable war file | ||||
| - Specify suitable Content-Type for downloaded files in the repository viewer | ||||
| - Fix some bugs | ||||
|  | ||||
| ### 1.5 - 4 Sep 2013 | ||||
| - Fork and pull request | ||||
| - LDAP authentication | ||||
| - Mail notification | ||||
| - Add an option to turn off the gravatar support | ||||
| - Add the branch tab in the repository viewer | ||||
| - Encoding auto detection for the file content in the repository viewer | ||||
| - Add favicon, header logo and icons for the timeline | ||||
| - Specify data directory via environment variable GITBUCKET_HOME | ||||
| - 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 | ||||
| - Added activity timeline. | ||||
| - Bugfix for Git 1.8.1.5 or later. | ||||
| - Allows multi-byte characters as label. | ||||
| - Fixed some bugs. | ||||
| - Add activity timeline | ||||
| - Bugfix for Git 1.8.1.5 or later | ||||
| - Allow multi-byte characters as label | ||||
| - Fix some bugs | ||||
|  | ||||
| ### 1.1 - 05 Jul 2013 | ||||
| - Fixed some bugs. | ||||
| - Upgrade to JGit 3.0. | ||||
| - Fix some bugs | ||||
| - Upgrade to JGit 3.0 | ||||
|  | ||||
| ### 1.0 - 04 Jul 2013 | ||||
| - This is a first public release. | ||||
| - This is a first public release | ||||
|   | ||||
							
								
								
									
										61
									
								
								build.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								build.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/javax.servlet-3.0.0.v201112011016.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/javax.servlet-3.0.0.v201112011016.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-continuation-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-continuation-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-http-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-http-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-io-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-io-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-security-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-security-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-server-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-server-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-servlet-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-servlet-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-util-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-util-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-webapp-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-webapp-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								embed-jetty/jetty-xml-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								embed-jetty/jetty-xml-8.1.8.v20121106.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -23,8 +23,8 @@ | ||||
|       <constraint> | ||||
|         <height>-1</height> | ||||
|         <width>-1</width> | ||||
|         <x>37</x> | ||||
|         <y>36</y> | ||||
|         <x>33</x> | ||||
|         <y>18</y> | ||||
|       </constraint> | ||||
|       <sourceConnections/> | ||||
|       <targetConnections> | ||||
| @@ -51,8 +51,8 @@ | ||||
|             <constraint> | ||||
|               <height>-1</height> | ||||
|               <width>-1</width> | ||||
|               <x>751</x> | ||||
|               <y>47</y> | ||||
|               <x>723</x> | ||||
|               <y>138</y> | ||||
|             </constraint> | ||||
|             <sourceConnections> | ||||
|               <net.java.amateras.db.visual.model.ForeignKeyModel> | ||||
| @@ -79,8 +79,8 @@ | ||||
|                   <constraint> | ||||
|                     <height>-1</height> | ||||
|                     <width>-1</width> | ||||
|                     <x>882</x> | ||||
|                     <y>239</y> | ||||
|                     <x>1182</x> | ||||
|                     <y>339</y> | ||||
|                   </constraint> | ||||
|                   <sourceConnections/> | ||||
|                   <targetConnections> | ||||
| @@ -108,8 +108,8 @@ | ||||
|                         <constraint> | ||||
|                           <height>-1</height> | ||||
|                           <width>-1</width> | ||||
|                           <x>940</x> | ||||
|                           <y>615</y> | ||||
|                           <x>1301</x> | ||||
|                           <y>836</y> | ||||
|                         </constraint> | ||||
|                         <sourceConnections> | ||||
|                           <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> | ||||
| @@ -138,8 +138,8 @@ | ||||
|                               <constraint> | ||||
|                                 <height>-1</height> | ||||
|                                 <width>-1</width> | ||||
|                                 <x>420</x> | ||||
|                                 <y>758</y> | ||||
|                                 <x>684</x> | ||||
|                                 <y>858</y> | ||||
|                               </constraint> | ||||
|                               <sourceConnections> | ||||
|                                 <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> | ||||
| @@ -167,8 +167,8 @@ | ||||
|                                     <constraint> | ||||
|                                       <height>-1</height> | ||||
|                                       <width>-1</width> | ||||
|                                       <x>307</x> | ||||
|                                       <y>356</y> | ||||
|                                       <x>293</x> | ||||
|                                       <y>478</y> | ||||
|                                     </constraint> | ||||
|                                     <sourceConnections> | ||||
|                                       <net.java.amateras.db.visual.model.ForeignKeyModel> | ||||
| @@ -210,8 +210,8 @@ | ||||
|                                           <constraint> | ||||
|                                             <height>-1</height> | ||||
|                                             <width>-1</width> | ||||
|                                             <x>641</x> | ||||
|                                             <y>569</y> | ||||
|                                             <x>875</x> | ||||
|                                             <y>677</y> | ||||
|                                           </constraint> | ||||
|                                           <sourceConnections> | ||||
|                                             <net.java.amateras.db.visual.model.ForeignKeyModel> | ||||
| @@ -283,9 +283,14 @@ | ||||
|                                               <defaultValue></defaultValue> | ||||
|                                             </net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                             <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                               <columnName>MILESTONE_NAME</columnName> | ||||
|                                               <logicalName>Milestone Name</logicalName> | ||||
|                                               <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel/columnType"/> | ||||
|                                               <columnName>TITLE</columnName> | ||||
|                                               <logicalName>Title</logicalName> | ||||
|                                               <columnType class="net.java.amateras.db.dialect.ColumnType"> | ||||
|                                                 <name>VARCHAR</name> | ||||
|                                                 <logicalName>文字列</logicalName> | ||||
|                                                 <supportSize>true</supportSize> | ||||
|                                                 <type>12</type> | ||||
|                                               </columnType> | ||||
|                                               <size>100</size> | ||||
|                                               <notNull>true</notNull> | ||||
|                                               <primaryKey>false</primaryKey> | ||||
| @@ -293,6 +298,49 @@ | ||||
|                                               <autoIncrement>false</autoIncrement> | ||||
|                                               <defaultValue></defaultValue> | ||||
|                                             </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> | ||||
|                                           <indices/> | ||||
|                                           <backgroundColor> | ||||
| @@ -350,6 +398,36 @@ | ||||
|                                           </entry> | ||||
|                                         </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" 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> | ||||
|                                     <targetConnections> | ||||
|                                       <net.java.amateras.db.visual.model.ForeignKeyModel> | ||||
| @@ -375,8 +453,8 @@ | ||||
|                                           <constraint> | ||||
|                                             <height>-1</height> | ||||
|                                             <width>-1</width> | ||||
|                                             <x>26</x> | ||||
|                                             <y>660</y> | ||||
|                                             <x>18</x> | ||||
|                                             <y>776</y> | ||||
|                                           </constraint> | ||||
|                                           <sourceConnections> | ||||
|                                             <net.java.amateras.db.visual.model.ForeignKeyModel reference="../../.."/> | ||||
| @@ -462,6 +540,22 @@ | ||||
|                                               <autoIncrement>true</autoIncrement> | ||||
|                                               <defaultValue></defaultValue> | ||||
|                                             </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> | ||||
|                                               <columnName>CONTENT</columnName> | ||||
| @@ -498,7 +592,7 @@ | ||||
|                                             <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                               <columnName>UPDATED_DATE</columnName> | ||||
|                                               <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> | ||||
|                                               <notNull>true</notNull> | ||||
|                                               <primaryKey>false</primaryKey> | ||||
| @@ -572,10 +666,11 @@ | ||||
|                                         <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[4]/references/entry/net.java.amateras.db.visual.model.ColumnModel[2]"/> | ||||
|                                       <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                         <columnName>TITLE</columnName> | ||||
|                                         <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> | ||||
|                                         <notNull>true</notNull> | ||||
|                                         <primaryKey>false</primaryKey> | ||||
| @@ -586,7 +681,7 @@ | ||||
|                                       <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                         <columnName>CONTENT</columnName> | ||||
|                                         <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> | ||||
|                                         <notNull>true</notNull> | ||||
|                                         <primaryKey>false</primaryKey> | ||||
| @@ -597,7 +692,7 @@ | ||||
|                                       <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                         <columnName>REGISTERED_DATE</columnName> | ||||
|                                         <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> | ||||
|                                         <notNull>true</notNull> | ||||
|                                         <primaryKey>false</primaryKey> | ||||
| @@ -608,7 +703,7 @@ | ||||
|                                       <net.java.amateras.db.visual.model.ColumnModel> | ||||
|                                         <columnName>UPDATED_DATE</columnName> | ||||
|                                         <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> | ||||
|                                         <notNull>true</notNull> | ||||
|                                         <primaryKey>false</primaryKey> | ||||
| @@ -801,8 +896,8 @@ | ||||
|                         <constraint> | ||||
|                           <height>-1</height> | ||||
|                           <width>-1</width> | ||||
|                           <x>388</x> | ||||
|                           <y>166</y> | ||||
|                           <x>481</x> | ||||
|                           <y>361</y> | ||||
|                         </constraint> | ||||
|                         <sourceConnections> | ||||
|                           <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 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> | ||||
|                       <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> | ||||
|                   <error></error> | ||||
|                   <linkedPath></linkedPath> | ||||
| @@ -1062,6 +1401,100 @@ | ||||
|         </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/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> | ||||
|       <error></error> | ||||
|       <linkedPath></linkedPath> | ||||
| @@ -1089,8 +1522,8 @@ | ||||
|         <net.java.amateras.db.visual.model.ColumnModel> | ||||
|           <columnName>PASSWORD</columnName> | ||||
|           <logicalName>Password</logicalName> | ||||
|           <columnType class="net.java.amateras.db.dialect.ColumnType" reference="../../net.java.amateras.db.visual.model.ColumnModel[2]/columnType"/> | ||||
|           <size>20</size> | ||||
|           <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>40</size> | ||||
|           <notNull>true</notNull> | ||||
|           <primaryKey>false</primaryKey> | ||||
|           <description></description> | ||||
| @@ -1098,18 +1531,18 @@ | ||||
|           <defaultValue></defaultValue> | ||||
|         </net.java.amateras.db.visual.model.ColumnModel> | ||||
|         <net.java.amateras.db.visual.model.ColumnModel> | ||||
|           <columnName>USER_TYPE</columnName> | ||||
|           <logicalName>User Type</logicalName> | ||||
|           <columnName>ADMINISTRATOR</columnName> | ||||
|           <logicalName>Administrator</logicalName> | ||||
|           <columnType class="net.java.amateras.db.dialect.ColumnType"> | ||||
|             <name>INT</name> | ||||
|             <logicalName>整数</logicalName> | ||||
|             <name>BOOLEAN</name> | ||||
|             <logicalName>真偽値</logicalName> | ||||
|             <supportSize>false</supportSize> | ||||
|             <type>4</type> | ||||
|             <type>16</type> | ||||
|           </columnType> | ||||
|           <size>10</size> | ||||
|           <notNull>true</notNull> | ||||
|           <primaryKey>false</primaryKey> | ||||
|           <description>0:Normal 1:Administrator</description> | ||||
|           <description></description> | ||||
|           <autoIncrement>false</autoIncrement> | ||||
|           <defaultValue>0</defaultValue> | ||||
|         </net.java.amateras.db.visual.model.ColumnModel> | ||||
| @@ -1157,6 +1590,33 @@ | ||||
|           <autoIncrement>false</autoIncrement> | ||||
|           <defaultValue></defaultValue> | ||||
|         </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> | ||||
|       <indices> | ||||
|         <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/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[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> | ||||
|   <dommains/> | ||||
|   <dialectName>H2</dialectName> | ||||
							
								
								
									
										751
									
								
								etc/icons.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										751
									
								
								etc/icons.svg
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								project/build.properties
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| sbt.version=0.12.3 | ||||
| @@ -2,6 +2,7 @@ import sbt._ | ||||
| import Keys._ | ||||
| import org.scalatra.sbt._ | ||||
| import org.scalatra.sbt.PluginKeys._ | ||||
| import sbt.ScalaVersion | ||||
| import twirl.sbt.TwirlPlugin._ | ||||
| import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys | ||||
|  | ||||
| @@ -9,8 +10,8 @@ object MyBuild extends Build { | ||||
|   val Organization = "jp.sf.amateras" | ||||
|   val Name = "gitbucket" | ||||
|   val Version = "0.0.1" | ||||
|   val ScalaVersion = "2.10.1" | ||||
|   val ScalatraVersion = "2.2.0" | ||||
|   val ScalaVersion = "2.10.3" | ||||
|   val ScalatraVersion = "2.2.1" | ||||
|  | ||||
|   lazy val project = Project ( | ||||
|     "gitbucket", | ||||
| @@ -20,24 +21,35 @@ object MyBuild extends Build { | ||||
|       name := Name, | ||||
|       version := Version, | ||||
|       scalaVersion := ScalaVersion, | ||||
|       resolvers += Classpaths.typesafeReleases, | ||||
|       resolvers ++= Seq( | ||||
|         Classpaths.typesafeReleases, | ||||
|         "amateras-repo" at "http://amateras.sourceforge.jp/mvn/" | ||||
|       ), | ||||
|       scalacOptions := Seq("-deprecation"), | ||||
|       libraryDependencies ++= Seq( | ||||
|         "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", | ||||
|         "org.apache.commons" % "commons-io" % "1.3.2", | ||||
|         "org.scalatra" %% "scalatra" % ScalatraVersion, | ||||
|         "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", | ||||
|         "org.scalatra" %% "scalatra-json" % ScalatraVersion, | ||||
|         "org.json4s" %% "json4s-jackson" % "3.2.4", | ||||
|         "org.json4s" %% "json4s-jackson" % "3.2.5", | ||||
|         "jp.sf.amateras" %% "scalatra-forms" % "0.0.2", | ||||
|         "commons-io" % "commons-io" % "2.4", | ||||
|         "org.pegdown" % "pegdown" % "1.3.0", | ||||
|         "org.pegdown" % "pegdown" % "1.4.1", | ||||
|         "org.apache.commons" % "commons-compress" % "1.5", | ||||
|         "org.apache.commons" % "commons-email" % "1.3.1", | ||||
|         "org.apache.httpcomponents" % "httpclient" % "4.3", | ||||
|         "com.typesafe.slick" %% "slick" % "1.0.1", | ||||
|         "com.h2database" % "h2" % "1.3.171", | ||||
|         "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime", | ||||
|         "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container", | ||||
|         "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")) | ||||
|         "com.novell.ldap" % "jldap" % "2009-10-07", | ||||
|         "com.h2database" % "h2" % "1.3.173", | ||||
|         "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", | ||||
|         "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", | ||||
|         "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")), | ||||
|         "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: _*) | ||||
|   ) | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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("com.timushev.sbt" % "sbt-updates" % "0.1.2") | ||||
|   | ||||
							
								
								
									
										1
									
								
								sbt.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								sbt.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1 @@ | ||||
| java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@" | ||||
							
								
								
									
										53
									
								
								src/main/java/JettyLauncher.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/main/java/JettyLauncher.java
									
									
									
									
									
										Normal 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,17 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <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> | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 473 B After Width: | Height: | Size: 3.0 KiB | 
							
								
								
									
										24
									
								
								src/main/resources/update/1_4.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/main/resources/update/1_4.sql
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										21
									
								
								src/main/resources/update/1_5.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/main/resources/update/1_5.sql
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										8
									
								
								src/main/resources/update/1_6.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/main/resources/update/1_6.sql
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										5
									
								
								src/main/resources/update/1_7.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/main/resources/update/1_7.sql
									
									
									
									
									
										Normal 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; | ||||
| @@ -5,8 +5,10 @@ import javax.servlet._ | ||||
| class ScalatraBootstrap extends LifeCycle { | ||||
|   override def init(context: ServletContext) { | ||||
|     context.mount(new IndexController, "/") | ||||
|     context.mount(new SearchController, "/") | ||||
|     context.mount(new FileUploadController, "/upload") | ||||
|     context.mount(new SignInController, "/*") | ||||
|     context.mount(new DashboardController, "/*") | ||||
|     context.mount(new UserManagementController, "/*") | ||||
|     context.mount(new SystemSettingsController, "/*") | ||||
|     context.mount(new CreateRepositoryController, "/*") | ||||
| @@ -16,6 +18,7 @@ class ScalatraBootstrap extends LifeCycle { | ||||
|     context.mount(new LabelsController, "/*") | ||||
|     context.mount(new MilestonesController, "/*") | ||||
|     context.mount(new IssuesController, "/*") | ||||
|     context.mount(new PullRequestsController, "/*") | ||||
|     context.mount(new RepositorySettingsController, "/*") | ||||
|  | ||||
|     val dir = new java.io.File(_root_.util.Directory.GitBucketHome) | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| package app | ||||
|  | ||||
| import service._ | ||||
| import util.{FileUtil, FileUploadUtil, OneselfAuthenticator} | ||||
| import util.{FileUtil, OneselfAuthenticator} | ||||
| import util.StringUtil._ | ||||
| import util.Directory._ | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import org.apache.commons.io.FileUtils | ||||
| import org.scalatra.FlashMapSupport | ||||
|  | ||||
| class AccountController extends AccountControllerBase | ||||
| @@ -16,15 +15,16 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa | ||||
|   self: SystemSettingsService with AccountService with RepositoryService with ActivityService | ||||
|     with OneselfAuthenticator => | ||||
|  | ||||
|   case class AccountNewForm(userName: String, password: String,mailAddress: String, | ||||
|   case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, | ||||
|                             url: Option[String], fileId: Option[String]) | ||||
|  | ||||
|   case class AccountEditForm(password: Option[String], mailAddress: String, | ||||
|   case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, | ||||
|                              url: Option[String], fileId: Option[String], clearImage: Boolean) | ||||
|  | ||||
|   val newForm = mapping( | ||||
|     "userName"    -> trim(label("User name"    , text(required, maxlength(100), identifier, uniqueUserName))), | ||||
|     "password"    -> trim(label("Password"     , text(required, maxlength(20)))), | ||||
|     "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))), | ||||
|     "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), | ||||
|     "url"         -> trim(label("URL"          , optional(text(maxlength(200))))), | ||||
|     "fileId"      -> trim(label("File ID"      , optional(text()))) | ||||
| @@ -32,6 +32,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa | ||||
|  | ||||
|   val editForm = mapping( | ||||
|     "password"    -> trim(label("Password"     , optional(text(maxlength(20))))), | ||||
|     "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))), | ||||
|     "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), | ||||
|     "url"         -> trim(label("URL"          , optional(text(maxlength(200))))), | ||||
|     "fileId"      -> trim(label("File ID"      , optional(text()))), | ||||
| @@ -43,12 +44,23 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa | ||||
|    */ | ||||
|   get("/:userName") { | ||||
|     val userName = params("userName") | ||||
|     getAccountByUserName(userName).map { x => | ||||
|     getAccountByUserName(userName).map { account => | ||||
|       params.getOrElse("tab", "repositories") match { | ||||
|         // 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 | ||||
|         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 | ||||
|   } | ||||
| @@ -74,6 +86,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa | ||||
|     getAccountByUserName(userName).map { account => | ||||
|       updateAccount(account.copy( | ||||
|         password    = form.password.map(sha1).getOrElse(account.password), | ||||
|         fullName    = form.fullName, | ||||
|         mailAddress = form.mailAddress, | ||||
|         url         = form.url)) | ||||
|  | ||||
| @@ -96,7 +109,7 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa | ||||
|  | ||||
|   post("/register", newForm){ form => | ||||
|     if(loadSystemSettings().allowAccountRegistration){ | ||||
|       createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url) | ||||
|       createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) | ||||
|       updateImage(form.userName, form.fileId, false) | ||||
|       redirect("/signin") | ||||
|     } else NotFound | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| package app | ||||
|  | ||||
| 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.json._ | ||||
| import org.json4s._ | ||||
| @@ -10,7 +12,9 @@ import org.apache.commons.io.FileUtils | ||||
| import model.Account | ||||
| import scala.Some | ||||
| 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. | ||||
| @@ -20,73 +24,94 @@ abstract class ControllerBase extends ScalatraFilter | ||||
|  | ||||
|   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. | ||||
|    */ | ||||
|   implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request) | ||||
|  | ||||
|   private def currentURL: String = { | ||||
|     val queryString = request.getQueryString | ||||
|   private def currentURL: String = defining(request.getQueryString){ queryString => | ||||
|     request.getRequestURI + (if(queryString != null) "?" + queryString else "") | ||||
|   } | ||||
|  | ||||
|   private def LoginAccount: Option[Account] = { | ||||
|     session.get("LOGIN_ACCOUNT") match { | ||||
|       case Some(x: Account) => Some(x) | ||||
|       case _ => None | ||||
|     } | ||||
|   } | ||||
|   private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) | ||||
|  | ||||
|   def ajaxGet(path : String)(action : => Any) : Route = { | ||||
|   def ajaxGet(path : String)(action : => Any) : Route = | ||||
|     super.get(path){ | ||||
|       request.setAttribute("AJAX", "true") | ||||
|       request.setAttribute(Keys.Request.Ajax, "true") | ||||
|       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 => | ||||
|       request.setAttribute("AJAX", "true") | ||||
|       request.setAttribute(Keys.Request.Ajax, "true") | ||||
|       action(form) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def ajaxPost(path : String)(action : => Any) : Route = { | ||||
|   def ajaxPost(path : String)(action : => Any) : Route = | ||||
|     super.post(path){ | ||||
|       request.setAttribute("AJAX", "true") | ||||
|       request.setAttribute(Keys.Request.Ajax, "true") | ||||
|       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 => | ||||
|       request.setAttribute("AJAX", "true") | ||||
|       request.setAttribute(Keys.Request.Ajax, "true") | ||||
|       action(form) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected def NotFound() = { | ||||
|     if(request.getAttribute("AJAX") == null){ | ||||
|       org.scalatra.NotFound(html.error("Not Found")) | ||||
|     } else { | ||||
|   protected def NotFound() = | ||||
|     if(request.hasAttribute(Keys.Request.Ajax)){ | ||||
|       org.scalatra.NotFound() | ||||
|     } else { | ||||
|       org.scalatra.NotFound(html.error("Not Found")) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected def Unauthorized()(implicit context: app.Context) = { | ||||
|     if(request.getAttribute("AJAX") == null){ | ||||
|   protected def Unauthorized()(implicit context: app.Context) = | ||||
|     if(request.hasAttribute(Keys.Request.Ajax)){ | ||||
|       org.scalatra.Unauthorized() | ||||
|     } else { | ||||
|       if(context.loginAccount.isDefined){ | ||||
|         org.scalatra.Unauthorized(redirect("/")) | ||||
|       } 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 = { | ||||
|     val url = request.getRequestURL.toString | ||||
|   protected def baseUrl = defining(request.getRequestURL.toString){ url => | ||||
|     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){ | ||||
|  | ||||
|   def redirectUrl = if(request.getParameter("redirect") != null){ | ||||
|     request.getParameter("redirect") | ||||
|   } else { | ||||
|     currentUrl | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get object from cache. | ||||
|    * | ||||
|    * If object has not been cached with the specified key then retrieves by given action. | ||||
|    * Cached object are available during a request. | ||||
|    */ | ||||
|   def cache[A](key: String)(action: => A): A = { | ||||
|     Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse { | ||||
|       val newObject = action | ||||
|       request.setAttribute("cache." + key, newObject) | ||||
|       newObject | ||||
|   def cache[A](key: String)(action: => A): A = | ||||
|     defining(Keys.Request.Cache(key)){ cacheKey => | ||||
|       Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse { | ||||
|         val newObject = action | ||||
|         request.setAttribute(cacheKey, newObject) | ||||
|         newObject | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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){ | ||||
|       getAccountByUserName(userName).flatMap(_.image).map { image => | ||||
|         new java.io.File(getUserUploadDir(userName), image).delete() | ||||
| @@ -126,26 +159,50 @@ trait AccountManagementControllerBase extends ControllerBase { self: AccountServ | ||||
|       } | ||||
|     } else { | ||||
|       fileId.map { fileId => | ||||
|         val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get) | ||||
|         val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get) | ||||
|         FileUtils.moveFile( | ||||
|           FileUploadUtil.getTemporaryFile(fileId), | ||||
|           getTemporaryFile(fileId), | ||||
|           new java.io.File(getUserUploadDir(userName), filename) | ||||
|         ) | ||||
|         updateAvatarImage(userName, Some(filename)) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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." } | ||||
|   } | ||||
|  | ||||
|   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) | ||||
|         .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) } | ||||
|         .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)) | ||||
|  | ||||
| } | ||||
| @@ -1,107 +1,193 @@ | ||||
| package app | ||||
|  | ||||
| import util.Directory._ | ||||
| import util.{JGitUtil, UsersAuthenticator} | ||||
| import util.ControlUtil._ | ||||
| import util._ | ||||
| import service._ | ||||
| import java.io.File | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.lib._ | ||||
| import org.apache.commons.io._ | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import org.eclipse.jgit.lib.PersonIdent | ||||
|  | ||||
| class CreateRepositoryController extends CreateRepositoryControllerBase | ||||
|   with RepositoryService with AccountService with WikiService with LabelsService with ActivityService | ||||
|   with UsersAuthenticator | ||||
|   with UsersAuthenticator with ReadableUsersAuthenticator | ||||
|  | ||||
| /** | ||||
|  * Creates new repository. | ||||
|  */ | ||||
| trait CreateRepositoryControllerBase extends ControllerBase { | ||||
|   self: RepositoryService with WikiService with LabelsService with ActivityService | ||||
|     with UsersAuthenticator => | ||||
|   self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService | ||||
|     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))), | ||||
|     "description"  -> trim(label("Description"    , optional(text()))), | ||||
|     "isPrivate"    -> trim(label("Repository Type", boolean())), | ||||
|     "createReadme" -> trim(label("Create README"  , boolean())) | ||||
|   )(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. | ||||
|    */ | ||||
|   get("/new")(usersOnly { | ||||
|     html.newrepo() | ||||
|     html.newrepo(getGroupsByUserName(context.loginAccount.get.userName)) | ||||
|   }) | ||||
|    | ||||
|   /** | ||||
|    * Create new repository. | ||||
|    */ | ||||
|   post("/new", form)(usersOnly { form => | ||||
|     val loginAccount  = context.loginAccount.get | ||||
|     val loginUserName = loginAccount.userName | ||||
|   post("/new", newForm)(usersOnly { form => | ||||
|     LockUtil.lock(s"${form.owner}/${form.name}/create"){ | ||||
|       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 | ||||
|     createRepository(form.name, loginUserName, form.description, form.isPrivate) | ||||
|         // Insert to the database at first | ||||
|         createRepository(form.name, form.owner, form.description, form.isPrivate) | ||||
|  | ||||
|     // Insert default labels | ||||
|     createLabel(loginUserName, form.name, "bug", "fc2929") | ||||
|     createLabel(loginUserName, form.name, "duplicate", "cccccc") | ||||
|     createLabel(loginUserName, form.name, "enhancement", "84b6eb") | ||||
|     createLabel(loginUserName, form.name, "invalid", "e6e6e6") | ||||
|     createLabel(loginUserName, form.name, "question", "cc317c") | ||||
|     createLabel(loginUserName, form.name, "wontfix", "ffffff") | ||||
|         // Add collaborators for group repository | ||||
|         if(ownerAccount.isGroupAccount){ | ||||
|           getGroupMembers(form.owner).foreach { userName => | ||||
|             addCollaborator(form.owner, form.name, userName) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|     // Create the actual repository | ||||
|     val gitdir = getRepositoryDir(loginUserName, form.name) | ||||
|     JGitUtil.initRepository(gitdir) | ||||
|         // Insert default labels | ||||
|         insertDefaultLabels(form.owner, form.name) | ||||
|  | ||||
|     if(form.createReadme){ | ||||
|       val tmpdir = getInitRepositoryDir(loginUserName, form.name) | ||||
|       try { | ||||
|         // Clone the repository | ||||
|         Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call | ||||
|         // Create the actual repository | ||||
|         val gitdir = getRepositoryDir(form.owner, form.name) | ||||
|         JGitUtil.initRepository(gitdir) | ||||
|  | ||||
|         // Create README.md | ||||
|         FileUtils.writeStringToFile(new File(tmpdir, "README.md"), | ||||
|           if(form.description.nonEmpty){ | ||||
|             form.name + "\n" + | ||||
|             "===============\n" + | ||||
|             "\n" + | ||||
|             form.description.get | ||||
|           } else { | ||||
|             form.name + "\n" + | ||||
|             "===============\n" | ||||
|           }, "UTF-8") | ||||
|         if(form.createReadme){ | ||||
|           FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir => | ||||
|             // Clone the repository | ||||
|             Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call | ||||
|  | ||||
|         val git = Git.open(tmpdir) | ||||
|         git.add.addFilepattern("README.md").call | ||||
|         git.commit.setMessage("Initial commit").call | ||||
|         git.push.call | ||||
|             // Create README.md | ||||
|             FileUtils.writeStringToFile(new File(tmpdir, "README.md"), | ||||
|               if(form.description.nonEmpty){ | ||||
|                 form.name + "\n" + | ||||
|                   "===============\n" + | ||||
|                   "\n" + | ||||
|                   form.description.get | ||||
|               } else { | ||||
|                 form.name + "\n" + | ||||
|                   "===============\n" | ||||
|               }, "UTF-8") | ||||
|  | ||||
|       } finally { | ||||
|         FileUtils.deleteDirectory(tmpdir) | ||||
|             val git = Git.open(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. | ||||
|    */ | ||||
|   private def unique: Constraint = new Constraint(){ | ||||
|     def validate(name: String, value: String): Option[String] = | ||||
|       getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.") | ||||
|     override def validate(name: String, value: String, params: Map[String, String]): Option[String] = | ||||
|       params.get("owner").flatMap { userName => | ||||
|         getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") | ||||
|       } | ||||
|   } | ||||
|    | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										109
									
								
								src/main/scala/app/DashboardController.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/main/scala/app/DashboardController.scala
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package app | ||||
|  | ||||
| import util.{FileUtil, FileUploadUtil} | ||||
| import _root_.util.{Keys, FileUtil} | ||||
| import util.ControlUtil._ | ||||
| import org.scalatra._ | ||||
| import org.scalatra.servlet.{MultipartConfig, FileUploadSupport} | ||||
| import org.apache.commons.io.FileUtils | ||||
| @@ -9,18 +10,18 @@ import org.apache.commons.io.FileUtils | ||||
|  * Provides Ajax based file upload functionality. | ||||
|  * | ||||
|  * 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 with FileUploadSupport with FlashMapSupport { | ||||
| class FileUploadController extends ScalatraServlet | ||||
|   with FileUploadSupport with FlashMapSupport with FileUploadControllerBase { | ||||
|  | ||||
|   configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) | ||||
|  | ||||
|   post("/image"){ | ||||
|     fileParams.get("file") match { | ||||
|       case Some(file) if(FileUtil.isImage(file.name)) => { | ||||
|         val fileId  = FileUploadUtil.generateFileId | ||||
|         FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get) | ||||
|         session += "upload_" + fileId -> file.name | ||||
|       case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId => | ||||
|         FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get) | ||||
|         session += Keys.Session.Upload(fileId) -> file.name | ||||
|         Ok(fileId) | ||||
|       } | ||||
|       case None => BadRequest | ||||
|   | ||||
| @@ -1,21 +1,38 @@ | ||||
| package app | ||||
|  | ||||
| import util._ | ||||
| import service._ | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
|  | ||||
| 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("/"){ | ||||
|     val loginAccount = context.loginAccount | ||||
|  | ||||
|     html.index(getRecentActivities(), | ||||
|       getAccessibleRepositories(loginAccount, baseUrl), | ||||
|       getVisibleRepositories(loginAccount, baseUrl), | ||||
|       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) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,9 @@ import jp.sf.amateras.scalatra.forms._ | ||||
|  | ||||
| import service._ | ||||
| import IssuesService._ | ||||
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator} | ||||
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys} | ||||
| import util.Implicits._ | ||||
| import util.ControlUtil._ | ||||
| import org.scalatra.Ok | ||||
|  | ||||
| class IssuesController extends IssuesControllerBase | ||||
| @@ -57,98 +59,100 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/:id")(referrersOnly { repository => | ||||
|     val owner   = repository.owner | ||||
|     val name    = repository.name | ||||
|     val issueId = params("id") | ||||
|  | ||||
|     getIssue(owner, name, issueId) map { | ||||
|       issues.html.issue( | ||||
|     defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => | ||||
|       getIssue(owner, name, issueId) map { | ||||
|         issues.html.issue( | ||||
|           _, | ||||
|           getComments(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, | ||||
|           getMilestones(owner, name), | ||||
|           getLabels(owner, name), | ||||
|           hasWritePermission(owner, name, context.loginAccount), | ||||
|           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) => | ||||
|     val owner    = repository.owner | ||||
|     val name     = repository.name | ||||
|     val writable = hasWritePermission(owner, name, context.loginAccount) | ||||
|     val userName = context.loginAccount.get.userName | ||||
|     defining(repository.owner, repository.name){ case (owner, name) => | ||||
|       val writable = hasWritePermission(owner, name, context.loginAccount) | ||||
|       val userName = context.loginAccount.get.userName | ||||
|  | ||||
|     // insert issue | ||||
|     val issueId = createIssue(owner, name, userName, form.title, form.content, | ||||
|       if(writable) form.assignedUserName else None, | ||||
|       if(writable) form.milestoneId else None) | ||||
|       // insert issue | ||||
|       val issueId = createIssue(owner, name, userName, form.title, form.content, | ||||
|         if(writable) form.assignedUserName else None, | ||||
|         if(writable) form.milestoneId else None) | ||||
|  | ||||
|     // insert labels | ||||
|     if(writable){ | ||||
|       form.labelNames.map { value => | ||||
|         val labels = getLabels(owner, name) | ||||
|         value.split(",").foreach { labelName => | ||||
|           labels.find(_.labelName == labelName).map { label => | ||||
|             registerIssueLabel(owner, name, issueId, label.labelId) | ||||
|       // insert labels | ||||
|       if(writable){ | ||||
|         form.labelNames.map { value => | ||||
|           val labels = getLabels(owner, name) | ||||
|           value.split(",").foreach { labelName => | ||||
|             labels.find(_.labelName == labelName).map { label => | ||||
|               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) => | ||||
|     val owner = repository.owner | ||||
|     val name  = repository.name | ||||
|  | ||||
|     getIssue(owner, name, params("id")).map { issue => | ||||
|       if(isEditable(owner, name, issue.openedUserName)){ | ||||
|         updateIssue(owner, name, issue.issueId, form.title, form.content) | ||||
|         redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId)) | ||||
|       } else Unauthorized | ||||
|     } getOrElse NotFound | ||||
|     defining(repository.owner, repository.name){ case (owner, name) => | ||||
|       getIssue(owner, name, params("id")).map { issue => | ||||
|         if(isEditable(owner, name, issue.openedUserName)){ | ||||
|           updateIssue(owner, name, issue.issueId, form.title, form.content) | ||||
|           redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") | ||||
|         } else Unauthorized | ||||
|       } getOrElse NotFound | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => | ||||
|     handleComment(form.issueId, Some(form.content), repository)() map { id => | ||||
|       redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id)) | ||||
|     handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => | ||||
|       redirect(s"/${repository.owner}/${repository.name}/${ | ||||
|         if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => | ||||
|     handleComment(form.issueId, form.content, repository)() map { id => | ||||
|       redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id)) | ||||
|     handleComment(form.issueId, form.content, repository)() map { case (issue, id) => | ||||
|       redirect(s"/${repository.owner}/${repository.name}/${ | ||||
|         if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => | ||||
|     val owner = repository.owner | ||||
|     val name  = repository.name | ||||
|  | ||||
|     getComment(owner, name, params("id")).map { comment => | ||||
|       if(isEditable(owner, name, comment.commentedUserName)){ | ||||
|         updateComment(comment.commentId, form.content) | ||||
|         redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId)) | ||||
|       } else Unauthorized | ||||
|     } getOrElse NotFound | ||||
|     defining(repository.owner, repository.name){ case (owner, name) => | ||||
|       getComment(owner, name, params("id")).map { comment => | ||||
|         if(isEditable(owner, name, comment.commentedUserName)){ | ||||
|           updateComment(comment.commentId, form.content) | ||||
|           redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") | ||||
|         } else Unauthorized | ||||
|       } getOrElse NotFound | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   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 => | ||||
|     val issueId = params("id").toInt | ||||
|  | ||||
|     registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) | ||||
|     issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) | ||||
|     defining(params("id").toInt){ issueId => | ||||
|       registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) | ||||
|       issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => | ||||
|     val issueId = params("id").toInt | ||||
|  | ||||
|     deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) | ||||
|     issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) | ||||
|     defining(params("id").toInt){ issueId => | ||||
|       deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) | ||||
|       issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => | ||||
| @@ -207,128 +211,148 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|  | ||||
|   ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => | ||||
|     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 => | ||||
|     val action = params.get("value") | ||||
|  | ||||
|     executeBatch(repository) { | ||||
|       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) | ||||
|     defining(params.get("value")){ action => | ||||
|       executeBatch(repository) { | ||||
|         handleComment(_, None, repository)( _ => action) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => | ||||
|     val value = assignedUserName("value") | ||||
|   post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => | ||||
|     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) { | ||||
|       updateAssignedUserName(repository.owner, repository.name, _, value) | ||||
|   post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => | ||||
|     defining(assignedUserName("value")){ value => | ||||
|       executeBatch(repository) { | ||||
|         updateAssignedUserName(repository.owner, repository.name, _, value) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => | ||||
|     val value = milestoneId("value") | ||||
|  | ||||
|     executeBatch(repository) { | ||||
|       updateMilestoneId(repository.owner, repository.name, _, value) | ||||
|     defining(milestoneId("value")){ value => | ||||
|       executeBatch(repository) { | ||||
|         updateMilestoneId(repository.owner, repository.name, _, value) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") | ||||
|   val milestoneId      = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt } | ||||
|   val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) | ||||
|  | ||||
|   private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = | ||||
|     hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName | ||||
|  | ||||
|   private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { | ||||
|     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) | ||||
|       (getAction: model.Issue => Option[String] = | ||||
|            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 => | ||||
|       val (action, recordActivity) = | ||||
|         getAction(issue) | ||||
|           .collect { | ||||
|             case "close"  => true  -> (Some("close")  -> Some(recordCloseIssueActivity _)) | ||||
|             case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _)) | ||||
|     defining(repository.owner, repository.name){ case (owner, name) => | ||||
|       val userName = context.loginAccount.get.userName | ||||
|  | ||||
|       getIssue(owner, name, issueId.toString) map { issue => | ||||
|         val (action, recordActivity) = | ||||
|           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) | ||||
|             t | ||||
|           } | ||||
|           .getOrElse(None -> None) | ||||
|             .getOrElse(None -> None) | ||||
|  | ||||
|       val commentId = content | ||||
|         val commentId = content | ||||
|           .map       ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) | ||||
|           .getOrElse ( action.get.capitalize -> action.get ) | ||||
|           match { | ||||
|             case (content, action) => createComment(owner, name, userName, issueId, content, action) | ||||
|           } | ||||
|         match { | ||||
|           case (content, action) => createComment(owner, name, userName, issueId, content, action) | ||||
|         } | ||||
|  | ||||
|       // record activity | ||||
|       content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) ) | ||||
|       recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) | ||||
|         // record activity | ||||
|         content foreach { | ||||
|           (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) = { | ||||
|     val owner      = repository.owner | ||||
|     val repoName   = repository.name | ||||
|     val userName   = if(filter != "all") Some(params("userName")) else None | ||||
|     val sessionKey = "%s/%s/issues".format(owner, repoName) | ||||
|     defining(repository.owner, repository.name){ case (owner, repoName) => | ||||
|       val filterUser = Map(filter -> params.getOrElse("userName", "")) | ||||
|       val page       = IssueSearchCondition.page(request) | ||||
|       val sessionKey = Keys.Session.Issues(owner, repoName) | ||||
|  | ||||
|     val page = try { | ||||
|       val i = params.getOrElse("page", "1").toInt | ||||
|       if(i <= 0) 1 else i | ||||
|     } catch { | ||||
|       case e: NumberFormatException => 1 | ||||
|       // retrieve search condition | ||||
|       val condition = session.putAndGet(sessionKey, | ||||
|         if(request.hasQueryString) IssueSearchCondition(request) | ||||
|         else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) | ||||
|       ) | ||||
|  | ||||
|       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)) | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -51,7 +51,7 @@ trait LabelsControllerBase extends ControllerBase { | ||||
|    * Constraint for the identifier such as user name, repository name or page name. | ||||
|    */ | ||||
|   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("^[^,]+$")){ | ||||
|         Some(s"${name} contains invalid character.") | ||||
|       } else if(value.startsWith("_") || value.startsWith("-")){ | ||||
|   | ||||
| @@ -3,7 +3,8 @@ package app | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
|  | ||||
| import service._ | ||||
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator} | ||||
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator} | ||||
| import util.Implicits._ | ||||
|  | ||||
| class MilestonesController extends MilestonesControllerBase | ||||
|   with MilestonesService with RepositoryService with AccountService | ||||
| @@ -39,34 +40,44 @@ trait MilestonesControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository => | ||||
|     issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository) | ||||
|     params("milestoneId").toIntOpt.map{ milestoneId => | ||||
|       issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) => | ||||
|     getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => | ||||
|       updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|     params("milestoneId").toIntOpt.flatMap{ milestoneId => | ||||
|       getMilestone(repository.owner, repository.name, milestoneId).map { milestone => | ||||
|         updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) | ||||
|         redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|       } | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => | ||||
|     getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => | ||||
|       closeMilestone(milestone) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|     params("milestoneId").toIntOpt.flatMap{ milestoneId => | ||||
|       getMilestone(repository.owner, repository.name, milestoneId).map { milestone => | ||||
|         closeMilestone(milestone) | ||||
|         redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|       } | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => | ||||
|     getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => | ||||
|       openMilestone(milestone) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|     params("milestoneId").toIntOpt.flatMap{ milestoneId => | ||||
|       getMilestone(repository.owner, repository.name, milestoneId).map { milestone => | ||||
|         openMilestone(milestone) | ||||
|         redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|       } | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => | ||||
|     getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone => | ||||
|       deleteMilestone(repository.owner, repository.name, milestone.milestoneId) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|     params("milestoneId").toIntOpt.flatMap{ milestoneId => | ||||
|       getMilestone(repository.owner, repository.name, milestoneId).map { milestone => | ||||
|         deleteMilestone(repository.owner, repository.name, milestone.milestoneId) | ||||
|         redirect(s"/${repository.owner}/${repository.name}/issues/milestones") | ||||
|       } | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   | ||||
							
								
								
									
										417
									
								
								src/main/scala/app/PullRequestsController.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								src/main/scala/app/PullRequestsController.scala
									
									
									
									
									
										Normal 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)) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -6,13 +6,20 @@ import util.{UsersAuthenticator, OwnerAuthenticator} | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import org.apache.commons.io.FileUtils | ||||
| 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 | ||||
|   with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator | ||||
|   with RepositoryService with AccountService with WebHookService | ||||
|   with OwnerAuthenticator with UsersAuthenticator | ||||
|  | ||||
| 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) | ||||
|    | ||||
|   val optionsForm = mapping( | ||||
| @@ -20,13 +27,21 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo | ||||
|     "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), | ||||
|     "isPrivate"     -> trim(label("Repository Type", boolean())) | ||||
|   )(OptionsForm.apply) | ||||
|    | ||||
|  | ||||
|   // for collaborator addition | ||||
|   case class CollaboratorForm(userName: String) | ||||
|  | ||||
|   val collaboratorForm = mapping( | ||||
|     "userName" -> trim(label("Username", text(required, collaborator))) | ||||
|   )(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. | ||||
|    */ | ||||
| @@ -45,7 +60,15 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo | ||||
|    * Save the repository options. | ||||
|    */ | ||||
|   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." | ||||
|     redirect(s"/${repository.owner}/${repository.name}/settings/options") | ||||
|   }) | ||||
| @@ -54,22 +77,19 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo | ||||
|    * Display the Collaborators page. | ||||
|    */ | ||||
|   get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => | ||||
|     settings.html.collaborators(getCollaborators(repository.owner, repository.name), 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)) | ||||
|     settings.html.collaborators( | ||||
|       getCollaborators(repository.owner, repository.name), | ||||
|       getAccountByUserName(repository.owner).get.isGroupAccount, | ||||
|       repository) | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * Add the collaborator. | ||||
|    */ | ||||
|   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") | ||||
|   }) | ||||
|  | ||||
| @@ -77,10 +97,62 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo | ||||
|    * Add the collaborator. | ||||
|    */ | ||||
|   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") | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * 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. | ||||
|    */ | ||||
| @@ -101,19 +173,25 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo | ||||
|     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. | ||||
|    */ | ||||
|   private def collaborator: Constraint = new Constraint(){ | ||||
|     def validate(name: String, value: String): Option[String] = { | ||||
|       val paths = request.getRequestURI.split("/") | ||||
|     override def validate(name: String, value: String): Option[String] = | ||||
|       getAccountByUserName(value) match { | ||||
|         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.") | ||||
|         case _    => None | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -2,7 +2,8 @@ package app | ||||
|  | ||||
| import util.Directory._ | ||||
| import util.Implicits._ | ||||
| import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil} | ||||
| import util.ControlUtil._ | ||||
| import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} | ||||
| import service._ | ||||
| import org.scalatra._ | ||||
| import java.io.File | ||||
| @@ -10,6 +11,7 @@ import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.lib._ | ||||
| import org.apache.commons.io.FileUtils | ||||
| import org.eclipse.jgit.treewalk._ | ||||
| import org.eclipse.jgit.api.errors.RefNotFoundException | ||||
|  | ||||
| class RepositoryViewerController extends RepositoryViewerControllerBase  | ||||
|   with RepositoryService with AccountService with ReferrerAuthenticator | ||||
| @@ -37,49 +39,29 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     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. | ||||
|    */ | ||||
|   get("/:owner/:repository/tree/:id/*")(referrersOnly { | ||||
|     fileList(_, params("id"), multiParams("splat").head) | ||||
|   }) | ||||
|    | ||||
|   /** | ||||
|    * Displays the commit list of the specified branch. | ||||
|    */ | ||||
|   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 | ||||
|       } | ||||
|   get("/:owner/:repository/tree/*")(referrersOnly { repository => | ||||
|     val (id, path) = splitPath(repository, multiParams("splat").head) | ||||
|     if(path.isEmpty){ | ||||
|       fileList(repository, id) | ||||
|     } else { | ||||
|       fileList(repository, id, path) | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|   /** | ||||
|    * Displays the commit list of the specified resource. | ||||
|    */ | ||||
|   get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository => | ||||
|     val branchName = params("branch") | ||||
|     val path       = multiParams("splat").head //.replaceFirst("^tree/.+?/", "") | ||||
|     val page       = params.getOrElse("page", "1").toInt | ||||
|   get("/:owner/:repository/commits/*")(referrersOnly { repository => | ||||
|     val (branchName, path) = splitPath(repository, multiParams("splat").head) | ||||
|     val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) | ||||
|  | ||||
|     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 { | ||||
|         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) => | ||||
|               view.helpers.date(commit1.time) == view.helpers.date(commit2.time) | ||||
|             }, page, hasNext) | ||||
| @@ -91,12 +73,11 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|   /** | ||||
|    * Displays the file content of the specified branch or commit. | ||||
|    */ | ||||
|   get("/:owner/:repository/blob/:id/*")(referrersOnly { repository => | ||||
|     val id         = params("id") // branch name or commit id | ||||
|     val raw        = params.get("raw").getOrElse("false").toBoolean | ||||
|     val path       = multiParams("splat").head //.replaceFirst("^tree/.+?/", "") | ||||
|   get("/:owner/:repository/blob/*")(referrersOnly { repository => | ||||
|     val (id, path) = splitPath(repository, multiParams("splat").head) | ||||
|     val raw = params.get("raw").getOrElse("false").toBoolean | ||||
|  | ||||
|     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)) | ||||
|  | ||||
|       @scala.annotation.tailrec | ||||
| @@ -105,19 +86,18 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|         case true => getPathObjectId(path, walk) | ||||
|       } | ||||
|  | ||||
|       val treeWalk = new TreeWalk(git.getRepository) | ||||
|       val objectId = try { | ||||
|       val objectId = using(new TreeWalk(git.getRepository)){ treeWalk => | ||||
|         treeWalk.addTree(revCommit.getTree) | ||||
|         treeWalk.setRecursive(true) | ||||
|         getPathObjectId(path, treeWalk) | ||||
|       } finally { | ||||
|         treeWalk.release | ||||
|       } | ||||
|  | ||||
|       if(raw){ | ||||
|         // Download | ||||
|         contentType = "application/octet-stream" | ||||
|         JGitUtil.getContent(git, objectId, false).get | ||||
|         defining(JGitUtil.getContent(git, objectId, false).get){ bytes => | ||||
|           contentType = FileUtil.getContentType(path, bytes) | ||||
|           bytes | ||||
|         } | ||||
|       } else { | ||||
|         // Viewer | ||||
|         val large  = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) | ||||
| @@ -127,7 +107,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|         val content = if(viewer == "other"){ | ||||
|           if(bytes.isDefined && FileUtil.isText(bytes.get)){ | ||||
|             // text | ||||
|             JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8"))) | ||||
|             JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray)) | ||||
|           } else { | ||||
|             // binary | ||||
|             JGitUtil.ContentInfo("binary", None) | ||||
| @@ -148,22 +128,39 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|   get("/:owner/:repository/commit/:id")(referrersOnly { repository => | ||||
|     val id = params("id") | ||||
|  | ||||
|     JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => | ||||
|       val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) | ||||
|  | ||||
|       repo.html.commit(id, new JGitUtil.CommitInfo(revCommit), | ||||
|         JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName), | ||||
|         repository, JGitUtil.getDiffs(git, id)) | ||||
|     using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => | ||||
|       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), | ||||
|             JGitUtil.getBranchesOfCommit(git, revCommit.getName), | ||||
|             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. | ||||
|    */ | ||||
|   get("/:owner/:repository/tags")(referrersOnly { | ||||
|     repo.html.tags(_) | ||||
|   }) | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Download repository contents as an archive. | ||||
|    */ | ||||
| @@ -180,11 +177,12 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|        | ||||
|       // clone the repository | ||||
|       val cloneDir = new File(workDir, revision) | ||||
|       JGitUtil.withGit(Git.cloneRepository | ||||
|       using(Git.cloneRepository | ||||
|           .setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString) | ||||
|           .setDirectory(cloneDir) | ||||
|           .setBranch(revision) | ||||
|           .call){ git => | ||||
|        | ||||
|  | ||||
|         // checkout the specified revision | ||||
|         git.checkout.setName(revision).call | ||||
|       } | ||||
| @@ -202,7 +200,29 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|       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. | ||||
|    *  | ||||
| @@ -215,26 +235,26 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     if(repository.commitCount == 0){ | ||||
|       repo.html.guide(repository) | ||||
|     } 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) | ||||
|         // get specified commit | ||||
|         revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) => | ||||
|           val revCommit = JGitUtil.getRevCommitFromId(git, objectId) | ||||
|  | ||||
|         JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => | ||||
|           defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit => | ||||
|           // get files | ||||
|           val files = JGitUtil.getFileList(git, revision, path) | ||||
|           // process README.md | ||||
|           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") | ||||
|           } | ||||
|             val files = JGitUtil.getFileList(git, revision, path) | ||||
|             // process README.md | ||||
|             val readme = files.find(_.name == "README.md").map { file => | ||||
|               StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) | ||||
|             } | ||||
|  | ||||
|           repo.html.files(revision, repository, | ||||
|             if(path == ".") Nil else path.split("/").toList, // current path | ||||
|             new JGitUtil.CommitInfo(revCommit), // latest commit | ||||
|             files, readme) | ||||
|             repo.html.files(revision, repository, | ||||
|               if(path == ".") Nil else path.split("/").toList, // current path | ||||
|               new JGitUtil.CommitInfo(revCommit), // latest commit | ||||
|               files, readme) | ||||
|           } | ||||
|         } getOrElse NotFound | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										52
									
								
								src/main/scala/app/SearchController.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/main/scala/app/SearchController.scala
									
									
									
									
									
										Normal 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) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
| } | ||||
| @@ -1,8 +1,9 @@ | ||||
| package app | ||||
|  | ||||
| import service._ | ||||
| import util.StringUtil._ | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import util.Implicits._ | ||||
| import util.Keys | ||||
|  | ||||
| class SignInController extends SignInControllerBase with SystemSettingsService with AccountService | ||||
|  | ||||
| @@ -16,27 +17,17 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService | ||||
|   )(SignInForm.apply) | ||||
|    | ||||
|   get("/signin"){ | ||||
|     val queryString = request.getQueryString | ||||
|     if(queryString != null && queryString.startsWith("/")){ | ||||
|       session.setAttribute("REDIRECT", queryString) | ||||
|     val redirect = params.get("redirect") | ||||
|     if(redirect.isDefined && redirect.get.startsWith("/")){ | ||||
|       session.setAttribute(Keys.Session.Redirect, redirect.get) | ||||
|     } | ||||
|     html.signin(loadSystemSettings()) | ||||
|   } | ||||
|  | ||||
|   post("/signin", form){ form => | ||||
|     val account = getAccountByUserName(form.userName) | ||||
|     if(account.isEmpty || account.get.password != sha1(form.password)){ | ||||
|       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("/") | ||||
|       } | ||||
|     authenticate(loadSystemSettings(), form.userName, form.password) match { | ||||
|       case Some(account) => signin(account) | ||||
|       case None          => redirect("/signin") | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -45,4 +36,22 @@ trait SignInControllerBase extends ControllerBase { self: SystemSettingsService | ||||
|     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("/") | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -12,11 +12,30 @@ class SystemSettingsController extends SystemSettingsControllerBase | ||||
| trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { | ||||
|   self: SystemSettingsService with AccountService with AdminAuthenticator => | ||||
|  | ||||
|   private case class SystemSettingsForm(allowAccountRegistration: Boolean) | ||||
|  | ||||
|   private val form = mapping( | ||||
|     "allowAccountRegistration" -> trim(label("Account registration", boolean())) | ||||
|   )(SystemSettingsForm.apply) | ||||
|     "allowAccountRegistration" -> trim(label("Account registration", boolean())), | ||||
|     "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 { | ||||
| @@ -24,7 +43,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { | ||||
|   }) | ||||
|  | ||||
|   post("/admin/system", form)(adminOnly { form => | ||||
|     saveSystemSettings(SystemSettings(form.allowAccountRegistration)) | ||||
|     saveSystemSettings(form) | ||||
|     flash += "info" -> "System settings has been updated." | ||||
|     redirect("/admin/system") | ||||
|   }) | ||||
|   | ||||
| @@ -1,67 +1,96 @@ | ||||
| package app | ||||
|  | ||||
| import service._ | ||||
| import util.{FileUploadUtil, FileUtil, AdminAuthenticator} | ||||
| import util.AdminAuthenticator | ||||
| import util.StringUtil._ | ||||
| import util.ControlUtil._ | ||||
| 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 { | ||||
|   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]) | ||||
|  | ||||
|   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) | ||||
|  | ||||
|   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))), | ||||
|     "password"    -> trim(label("Password"     , text(required, maxlength(20)))), | ||||
|     "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))), | ||||
|     "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), | ||||
|     "isAdmin"     -> trim(label("User Type"    , boolean())), | ||||
|     "url"         -> trim(label("URL"          , optional(text(maxlength(200))))), | ||||
|     "fileId"      -> trim(label("File ID"      , optional(text()))) | ||||
|   )(UserNewForm.apply) | ||||
|   )(NewUserForm.apply) | ||||
|  | ||||
|   val editForm = mapping( | ||||
|   val editUserForm = mapping( | ||||
|     "userName"    -> trim(label("Username"     , text(required, maxlength(100), identifier))), | ||||
|     "password"    -> trim(label("Password"     , optional(text(maxlength(20))))), | ||||
|     "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))), | ||||
|     "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), | ||||
|     "isAdmin"     -> trim(label("User Type"    , boolean())), | ||||
|     "url"         -> trim(label("URL"          , optional(text(maxlength(200))))), | ||||
|     "fileId"      -> trim(label("File ID"      , optional(text()))), | ||||
|     "clearImage"  -> trim(label("Clear image"  , boolean())) | ||||
|   )(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 { | ||||
|     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 { | ||||
|     admin.users.html.edit(None) | ||||
|   get("/admin/users/_newuser")(adminOnly { | ||||
|     admin.users.html.user(None) | ||||
|   }) | ||||
|    | ||||
|   post("/admin/users/_new", newForm)(adminOnly { form => | ||||
|     createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url) | ||||
|   post("/admin/users/_newuser", newUserForm)(adminOnly { form => | ||||
|     createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) | ||||
|     updateImage(form.userName, form.fileId, false) | ||||
|     redirect("/admin/users") | ||||
|   }) | ||||
|    | ||||
|   get("/admin/users/:userName/_edit")(adminOnly { | ||||
|   get("/admin/users/:userName/_edituser")(adminOnly { | ||||
|     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") | ||||
|     getAccountByUserName(userName).map { account => | ||||
|       updateAccount(getAccountByUserName(userName).get.copy( | ||||
|         password     = form.password.map(sha1).getOrElse(account.password), | ||||
|         fullName     = form.fullName, | ||||
|         mailAddress  = form.mailAddress, | ||||
|         isAdmin      = form.isAdmin, | ||||
|         url          = form.url)) | ||||
| @@ -71,5 +100,46 @@ trait UserManagementControllerBase extends AccountManagementControllerBase { | ||||
|  | ||||
|     } 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 | ||||
|   }) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,32 +1,39 @@ | ||||
| package app | ||||
|  | ||||
| import service._ | ||||
| import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil} | ||||
| import util._ | ||||
| import util.Directory._ | ||||
| import util.ControlUtil._ | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.scalatra.FlashMapSupport | ||||
| import service.WikiService.WikiPageInfo | ||||
| import scala.Some | ||||
|  | ||||
| class WikiController extends WikiControllerBase  | ||||
|   with WikiService with RepositoryService with AccountService with ActivityService | ||||
|   with CollaboratorsAuthenticator with ReferrerAuthenticator | ||||
|  | ||||
| trait WikiControllerBase extends ControllerBase { | ||||
| trait WikiControllerBase extends ControllerBase with FlashMapSupport { | ||||
|   self: WikiService with RepositoryService with ActivityService | ||||
|     with CollaboratorsAuthenticator with ReferrerAuthenticator => | ||||
|  | ||||
|   case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String) | ||||
|   case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) | ||||
|    | ||||
|   val newForm = mapping( | ||||
|     "pageName"        -> trim(label("Page name"          , text(required, maxlength(40), pagename, unique))), | ||||
|     "content"         -> trim(label("Content"            , text(required))), | ||||
|     "message"         -> trim(label("Message"            , optional(text()))), | ||||
|     "currentPageName" -> trim(label("Current page name"  , text())) | ||||
|     "pageName"        -> trim(label("Page name"         , text(required, maxlength(40), pagename, unique))), | ||||
|     "content"         -> trim(label("Content"           , text(required, conflictForNew))), | ||||
|     "message"         -> trim(label("Message"           , optional(text()))), | ||||
|     "currentPageName" -> trim(label("Current page name" , text())), | ||||
|     "id"              -> trim(label("Latest commit id"  , text())) | ||||
|   )(WikiPageEditForm.apply) | ||||
|    | ||||
|   val editForm = mapping( | ||||
|     "pageName"        -> trim(label("Page name"          , text(required, maxlength(40), pagename))), | ||||
|     "content"         -> trim(label("Content"            , text(required))), | ||||
|     "message"         -> trim(label("Message"            , optional(text()))), | ||||
|     "currentPageName" -> trim(label("Current page name"  , text(required))) | ||||
|     "pageName"        -> trim(label("Page name"         , text(required, maxlength(40), pagename))), | ||||
|     "content"         -> trim(label("Content"           , text(required, conflictForEdit))), | ||||
|     "message"         -> trim(label("Message"           , optional(text()))), | ||||
|     "currentPageName" -> trim(label("Current page name" , text(required))), | ||||
|     "id"              -> trim(label("Latest commit id"  , text(required))) | ||||
|   )(WikiPageEditForm.apply) | ||||
|    | ||||
|   get("/:owner/:repository/wiki")(referrersOnly { repository => | ||||
| @@ -40,13 +47,13 @@ trait WikiControllerBase extends ControllerBase { | ||||
|  | ||||
|     getWikiPage(repository.owner, repository.name, pageName).map { page => | ||||
|       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 => | ||||
|     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 { | ||||
|         case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository) | ||||
|         case Left(_) => NotFound | ||||
| @@ -56,36 +63,60 @@ trait WikiControllerBase extends ControllerBase { | ||||
|    | ||||
|   get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => | ||||
|     val pageName = StringUtil.urlDecode(params("page")) | ||||
|     val commitId = params("commitId").split("\\.\\.\\.") | ||||
|     val Array(from, to) = params("commitId").split("\\.\\.\\.") | ||||
|  | ||||
|     JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => | ||||
|       wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository) | ||||
|     using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => | ||||
|       wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository, | ||||
|         hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|   get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => | ||||
|     val commitId   = params("commitId").split("\\.\\.\\.") | ||||
|     val Array(from, to) = params("commitId").split("\\.\\.\\.") | ||||
|  | ||||
|     JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => | ||||
|       wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository) | ||||
|     using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => | ||||
|       wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, | ||||
|         hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|  | ||||
|   get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository => | ||||
|     val pageName = StringUtil.urlDecode(params("page")) | ||||
|     val Array(from, to) = params("commitId").split("\\.\\.\\.") | ||||
|  | ||||
|     if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){ | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") | ||||
|     } else { | ||||
|       flash += "info" -> "This patch was not able to be reversed." | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}") | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository => | ||||
|     val Array(from, to) = params("commitId").split("\\.\\.\\.") | ||||
|  | ||||
|     if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){ | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/}") | ||||
|     } else { | ||||
|       flash += "info" -> "This patch was not able to be reversed." | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository => | ||||
|     val pageName = StringUtil.urlDecode(params("page")) | ||||
|     wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) | ||||
|   }) | ||||
|    | ||||
|   post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => | ||||
|     val loginAccount = context.loginAccount.get | ||||
|      | ||||
|     saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, | ||||
|         form.content, loginAccount, form.message.getOrElse("")) | ||||
|      | ||||
|     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)}") | ||||
|     defining(context.loginAccount.get){ loginAccount => | ||||
|       saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, | ||||
|           form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId => | ||||
|         updateLastActivityDate(repository.owner, repository.name) | ||||
|         recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) | ||||
|       } | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|   get("/:owner/:repository/wiki/_new")(collaboratorsOnly { | ||||
| @@ -93,24 +124,26 @@ trait WikiControllerBase extends ControllerBase { | ||||
|   }) | ||||
|    | ||||
|   post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => | ||||
|     val loginAccount = context.loginAccount.get | ||||
|      | ||||
|     saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, | ||||
|         form.content, context.loginAccount.get, form.message.getOrElse("")) | ||||
|      | ||||
|     updateLastActivityDate(repository.owner, repository.name) | ||||
|     recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) | ||||
|     defining(context.loginAccount.get){ loginAccount => | ||||
|       saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, | ||||
|           form.content, loginAccount, form.message.getOrElse(""), None) | ||||
|  | ||||
|     redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") | ||||
|       updateLastActivityDate(repository.owner, repository.name) | ||||
|       recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) | ||||
|  | ||||
|       redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") | ||||
|     } | ||||
|   }) | ||||
|    | ||||
|   get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository => | ||||
|       val pageName = StringUtil.urlDecode(params("page")) | ||||
|      | ||||
|     deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}") | ||||
|     updateLastActivityDate(repository.owner, repository.name) | ||||
|     val pageName = StringUtil.urlDecode(params("page")) | ||||
|  | ||||
|     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 => | ||||
| @@ -119,7 +152,7 @@ trait WikiControllerBase extends ControllerBase { | ||||
|   }) | ||||
|    | ||||
|   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 { | ||||
|         case Right((logs, hasNext)) => wiki.html.history(None, logs, repository) | ||||
|         case Left(_) => NotFound | ||||
| @@ -128,19 +161,21 @@ trait WikiControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => | ||||
|     getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content => | ||||
|         contentType = "application/octet-stream" | ||||
|         content | ||||
|     val path = multiParams("splat").head | ||||
|  | ||||
|     getFileContent(repository.owner, repository.name, path).map { bytes => | ||||
|       contentType = FileUtil.getContentType(path, bytes) | ||||
|       bytes | ||||
|     } getOrElse NotFound | ||||
|   }) | ||||
|  | ||||
|   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.") | ||||
|   } | ||||
|  | ||||
|   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(_))){ | ||||
|         Some(s"${name} contains invalid character.") | ||||
|       } 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")) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import scala.slick.driver.H2Driver.simple._ | ||||
|  | ||||
| object Accounts extends Table[Account]("ACCOUNT") { | ||||
|   def userName = column[String]("USER_NAME", O PrimaryKey) | ||||
|   def fullName = column[String]("FULL_NAME") | ||||
|   def mailAddress = column[String]("MAIL_ADDRESS") | ||||
|   def password = column[String]("PASSWORD") | ||||
|   def isAdmin = column[Boolean]("ADMINISTRATOR") | ||||
| @@ -12,11 +13,13 @@ object Accounts extends Table[Account]("ACCOUNT") { | ||||
|   def updatedDate = column[java.util.Date]("UPDATED_DATE") | ||||
|   def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") | ||||
|   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( | ||||
|     userName: String, | ||||
|     fullName: String, | ||||
|     mailAddress: String, | ||||
|     password: String, | ||||
|     isAdmin: Boolean, | ||||
| @@ -24,5 +27,6 @@ case class Account( | ||||
|     registeredDate: java.util.Date, | ||||
|     updatedDate: java.util.Date, | ||||
|     lastLoginDate: Option[java.util.Date], | ||||
|     image: Option[String] | ||||
|     image: Option[String], | ||||
|     isGroupAccount: Boolean | ||||
| ) | ||||
|   | ||||
							
								
								
									
										14
									
								
								src/main/scala/model/GroupMembers.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/main/scala/model/GroupMembers.scala
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
| @@ -7,6 +7,11 @@ object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTempla | ||||
|   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 { | ||||
|   def openedUserName = column[String]("OPENED_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 registeredDate = column[java.util.Date]("REGISTERED_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) | ||||
| } | ||||
| @@ -31,4 +37,5 @@ case class Issue( | ||||
|     content: Option[String], | ||||
|     closed: Boolean, | ||||
|     registeredDate: java.util.Date, | ||||
|     updatedDate: java.util.Date) | ||||
|     updatedDate: java.util.Date, | ||||
|     isPullRequest: Boolean) | ||||
							
								
								
									
										28
									
								
								src/main/scala/model/PullRequest.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/main/scala/model/PullRequest.scala
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
| @@ -9,7 +9,11 @@ object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate { | ||||
|   def registeredDate = column[java.util.Date]("REGISTERED_DATE") | ||||
|   def updatedDate = column[java.util.Date]("UPDATED_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) | ||||
| } | ||||
| @@ -22,5 +26,9 @@ case class Repository( | ||||
|   defaultBranch: String, | ||||
|   registeredDate: 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] | ||||
| ) | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/main/scala/model/WebHook.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/main/scala/model/WebHook.scala
									
									
									
									
									
										Normal 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 | ||||
| ) | ||||
| @@ -1,5 +1,6 @@ | ||||
| package object model { | ||||
|   import scala.slick.lifted.MappedTypeMapper | ||||
|   import scala.slick.driver.BasicDriver.Implicit._ | ||||
|   import scala.slick.lifted.{Column, MappedTypeMapper} | ||||
|  | ||||
|   // java.util.Date TypeMapper | ||||
|   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) | ||||
|   ) | ||||
|  | ||||
|   implicit class RichColumn(c1: Column[Boolean]){ | ||||
|     def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns system date. | ||||
|    */ | ||||
|   | ||||
| @@ -3,9 +3,54 @@ package service | ||||
| import model._ | ||||
| import scala.slick.driver.H2Driver.simple._ | ||||
| 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 { | ||||
|  | ||||
|   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] =  | ||||
|     Query(Accounts) filter(_.userName is userName.bind) firstOption | ||||
|  | ||||
| @@ -14,24 +59,27 @@ trait AccountService { | ||||
|  | ||||
|   def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list | ||||
|      | ||||
|   def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit = | ||||
|   def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit = | ||||
|     Accounts insert Account( | ||||
|       userName       = userName, | ||||
|       password       = password, | ||||
|       fullName       = fullName, | ||||
|       mailAddress    = mailAddress, | ||||
|       isAdmin        = isAdmin, | ||||
|       url            = url, | ||||
|       registeredDate = currentDate, | ||||
|       updatedDate    = currentDate, | ||||
|       lastLoginDate  = None, | ||||
|       image          = None) | ||||
|       image          = None, | ||||
|       isGroupAccount = false) | ||||
|  | ||||
|   def updateAccount(account: Account): Unit =  | ||||
|     Accounts | ||||
|       .filter { a => a.userName is account.userName.bind } | ||||
|       .map    { a => a.password ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? } | ||||
|       .map    { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? } | ||||
|       .update ( | ||||
|         account.password,  | ||||
|         account.fullName,  | ||||
|         account.mailAddress,  | ||||
|         account.isAdmin, | ||||
|         account.url, | ||||
| @@ -44,5 +92,43 @@ trait AccountService { | ||||
|  | ||||
|   def updateLastLoginDate(userName: String): Unit = | ||||
|     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 | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -6,23 +6,23 @@ import Database.threadLocalSession | ||||
|  | ||||
| trait ActivityService { | ||||
|  | ||||
|   def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = { | ||||
|     val q = Query(Activities) | ||||
|   def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = | ||||
|     Activities | ||||
|       .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] = | ||||
|     Query(Activities) | ||||
|     Activities | ||||
|       .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) | ||||
|       .filter { case (t1, t2) => t2.isPrivate is false.bind } | ||||
|       .sortBy { case (t1, t2) => t1.activityId desc } | ||||
| @@ -52,6 +52,13 @@ trait ActivityService { | ||||
|       Some(title), | ||||
|       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 = | ||||
|     Activities.autoInc insert(userName, repositoryName, activityUserName, | ||||
|       "reopen_issue", | ||||
| @@ -65,7 +72,14 @@ trait ActivityService { | ||||
|       s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(cut(comment, 200)), | ||||
|       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) = | ||||
|     Activities.autoInc insert(userName, repositoryName, activityUserName, | ||||
|       "create_wiki", | ||||
| @@ -73,11 +87,11 @@ trait ActivityService { | ||||
|       Some(pageName), | ||||
|       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, | ||||
|       "edit_wiki", | ||||
|       s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", | ||||
|       Some(pageName), | ||||
|       Some(pageName + ":" + commitId), | ||||
|       currentDate) | ||||
|    | ||||
|   def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,  | ||||
| @@ -98,15 +112,42 @@ trait ActivityService { | ||||
|    | ||||
|   def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) = | ||||
|     Activities.autoInc insert(userName, repositoryName, activityUserName, | ||||
|       "create_tag", | ||||
|       "create_branch", | ||||
|       s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       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) = { | ||||
|     CommitLog insert (userName, repositoryName, commitId) | ||||
|   } | ||||
|    | ||||
|  | ||||
|   def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) = | ||||
|     CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*) | ||||
|  | ||||
|   def getAllCommitIds(userName: String, repositoryName: String): List[String] = | ||||
|     Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list | ||||
|  | ||||
|   def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean = | ||||
|     Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined | ||||
|    | ||||
|   | ||||
| @@ -6,8 +6,8 @@ import scala.slick.jdbc.{StaticQuery => Q} | ||||
| import Q.interpolation | ||||
|  | ||||
| import model._ | ||||
| import util.StringUtil._ | ||||
| import util.Implicits._ | ||||
| import util.StringUtil._ | ||||
|  | ||||
| trait IssuesService { | ||||
|   import IssuesService._ | ||||
| @@ -42,33 +42,28 @@ trait IssuesService { | ||||
|   /** | ||||
|    * 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 filter the filter type ("all", "assigned" or "created_by") | ||||
|    * @param userName the filter user name required for "assigned" and "created_by" | ||||
|    * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) | ||||
|    * @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 | ||||
|    */ | ||||
|   def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = { | ||||
|     // TODO It must be _.length instead of map (_.issueId) list).length. | ||||
|     //       But it does not work on Slick 1.0.1 (worked on Slick 1.0.0). | ||||
|     //       https://github.com/slick/slick/issues/170 | ||||
|     (searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length | ||||
|   } | ||||
|   def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, | ||||
|                  repos: (String, String)*): Int = | ||||
|     Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first | ||||
|   /** | ||||
|    * Returns the Map which contains issue count for each labels. | ||||
|    * | ||||
|    * @param owner the repository owner | ||||
|    * @param repository the repository name | ||||
|    * @param condition the search condition | ||||
|    * @param filter the filter type ("all", "assigned" or "created_by") | ||||
|    * @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), | ||||
|    * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) | ||||
|    * @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, | ||||
|                               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) => | ||||
|         t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) | ||||
|       } | ||||
| @@ -83,76 +78,100 @@ trait IssuesService { | ||||
|       } | ||||
|       .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. | ||||
|    * | ||||
|    * @param owner the repository owner | ||||
|    * @param repository the repository name | ||||
|    * @param condition the search condition | ||||
|    * @param filter the filter type ("all", "assigned" or "created_by") | ||||
|    * @param userName the filter user name required for "assigned" and "created_by" | ||||
|    * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_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 offset the offset 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) | ||||
|    */ | ||||
|   def searchIssue(owner: String, repository: String, condition: IssueSearchCondition, | ||||
|                   filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = { | ||||
|   def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, | ||||
|                   offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { | ||||
|  | ||||
|     // get issues and comment count | ||||
|     val issues = searchIssueQuery(owner, repository, condition, filter, userName) | ||||
|       .leftJoin(Query(IssueComments) | ||||
|         .filter  { t => | ||||
|           (t.byRepository(owner, repository)) && | ||||
|           (t.action inSetBind Seq("comment", "close_comment", "reopen_comment")) | ||||
|     // get issues and comment count and labels | ||||
|     searchIssueQuery(repos, condition, filterUser, onlyPullRequest) | ||||
|         .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } | ||||
|         .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } | ||||
|         .leftJoin (Labels)      .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } | ||||
|         .map { case (((t1, t2), t3), t4) => | ||||
|           (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) | ||||
|         } | ||||
|         .groupBy { _.issueId } | ||||
|         .map     { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1) | ||||
|       .sortBy { case (t1, t2) => | ||||
|         (condition.sort match { | ||||
|           case "created"  => t1.registeredDate | ||||
|           case "comments" => t2._2 | ||||
|           case "updated"  => t1.updatedDate | ||||
|         }) match { | ||||
|           case sort => condition.direction match { | ||||
|             case "asc"  => sort asc | ||||
|             case "desc" => sort desc | ||||
|         .sortBy(_._4)	// labelName | ||||
|         .sortBy { case (t1, commentCount, _,_,_) => | ||||
|           (condition.sort match { | ||||
|             case "created"  => t1.registeredDate | ||||
|             case "comments" => commentCount | ||||
|             case "updated"  => t1.updatedDate | ||||
|           }) match { | ||||
|             case sort => condition.direction match { | ||||
|               case "asc"  => sort asc | ||||
|               case "desc" => sort desc | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       .map { case (t1, t2) => (t1, t2._2.ifNull(0)) } | ||||
|       .drop(offset).take(limit) | ||||
|       .list | ||||
|  | ||||
|     // get labels | ||||
|     val labels = Query(IssueLabels) | ||||
|       .innerJoin(Labels).on { (t1, t2) => | ||||
|         t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) | ||||
|       } | ||||
|       .filter { case (t1, t2) => | ||||
|         (t1.byRepository(owner, repository)) && | ||||
|         (t1.issueId inSetBind (issues.map(_._1.issueId))) | ||||
|       } | ||||
|       .sortBy { case (t1, t2) => t1.issueId ~ t2.labelName } | ||||
|       .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) | ||||
|     } | ||||
|         .drop(offset).take(limit) | ||||
|         .list | ||||
|         .splitWith { (c1, c2) => | ||||
|           c1._1.userName == c2._1.userName && | ||||
|           c1._1.repositoryName == c2._1.repositoryName && | ||||
|           c1._1.issueId == c2._1.issueId | ||||
|         } | ||||
|         .map { issues => issues.head match { | ||||
|           case (issue, commentCount, _,_,_) => | ||||
|             (issue, | ||||
|              issues.flatMap { t => t._3.map ( | ||||
|                  Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) | ||||
|              )} toList, | ||||
|              commentCount) | ||||
|         }} toList | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 => | ||||
|       (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.milestoneId      is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && | ||||
|       (t1.milestoneId      isNull, condition.milestoneId == Some(None)) && | ||||
|       (t1.assignedUserName is userName.get.bind, filter == "assigned") && | ||||
|       (t1.openedUserName   is userName.get.bind, filter == "created_by") && | ||||
|       (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && | ||||
|       (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 => | ||||
|         (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && | ||||
|         (t2.labelId in | ||||
| @@ -164,7 +183,7 @@ trait IssuesService { | ||||
|     } | ||||
|  | ||||
|   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 | ||||
|     sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] | ||||
|         .firstOption.filter { id => | ||||
| @@ -179,7 +198,8 @@ trait IssuesService { | ||||
|           content, | ||||
|           false, | ||||
|           currentDate, | ||||
|           currentDate) | ||||
|           currentDate, | ||||
|           isPullRequest) | ||||
|  | ||||
|       // increment issue id | ||||
|       IssueId | ||||
| @@ -237,6 +257,60 @@ trait IssuesService { | ||||
|       } | ||||
|       .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 { | ||||
| @@ -247,6 +321,7 @@ object IssuesService { | ||||
|   case class IssueSearchCondition( | ||||
|       labels: Set[String] = Set.empty, | ||||
|       milestoneId: Option[Option[Int]] = None, | ||||
|       repo: Option[String] = None, | ||||
|       state: String = "open", | ||||
|       sort: String = "created", | ||||
|       direction: String = "desc"){ | ||||
| @@ -258,6 +333,7 @@ object IssuesService { | ||||
|           case Some(x) => x.toString | ||||
|           case None    => "none" | ||||
|         })}, | ||||
|         repo.map("for="   + urlEncode(_)), | ||||
|         Some("state="     + urlEncode(state)), | ||||
|         Some("sort="      + urlEncode(sort)), | ||||
|         Some("direction=" + urlEncode(direction))).flatten.mkString("&") | ||||
| @@ -274,12 +350,21 @@ object IssuesService { | ||||
|     def apply(request: HttpServletRequest): IssueSearchCondition = | ||||
|       IssueSearchCondition( | ||||
|         param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty), | ||||
|         param(request, "milestone").map(_ match { | ||||
|         param(request, "milestone").map{ | ||||
|           case "none" => None | ||||
|           case x      => Some(x.toInt) | ||||
|         }), | ||||
|           case x      => x.toIntOpt | ||||
|         }, | ||||
|         param(request, "for"), | ||||
|         param(request, "state",     Seq("open", "closed")).getOrElse("open"), | ||||
|         param(request, "sort",      Seq("created", "comments", "updated")).getOrElse("created"), | ||||
|         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 | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										54
									
								
								src/main/scala/service/PullRequestService.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/main/scala/service/PullRequestService.scala
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
| } | ||||
							
								
								
									
										126
									
								
								src/main/scala/service/RepositorySearchService.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/main/scala/service/RepositorySearchService.scala
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
| } | ||||
| @@ -15,19 +15,27 @@ trait RepositoryService { self: AccountService => | ||||
|    * @param userName the user name of the repository owner | ||||
|    * @param description the repository description | ||||
|    * @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 | ||||
|       Repository( | ||||
|         userName         = userName, | ||||
|         repositoryName   = repositoryName, | ||||
|         isPrivate        = isPrivate, | ||||
|         description      = description, | ||||
|         defaultBranch    = "master", | ||||
|         registeredDate   = currentDate, | ||||
|         updatedDate      = currentDate, | ||||
|         lastActivityDate = currentDate) | ||||
|      | ||||
|         userName             = userName, | ||||
|         repositoryName       = repositoryName, | ||||
|         isPrivate            = isPrivate, | ||||
|         description          = description, | ||||
|         defaultBranch        = "master", | ||||
|         registeredDate       = currentDate, | ||||
|         updatedDate          = currentDate, | ||||
|         lastActivityDate     = currentDate, | ||||
|         originUserName       = originUserName, | ||||
|         originRepositoryName = originRepositoryName, | ||||
|         parentUserName       = parentUserName, | ||||
|         parentRepositoryName = parentRepositoryName) | ||||
|  | ||||
|     IssueId insert (userName, repositoryName, 0) | ||||
|   } | ||||
|  | ||||
| @@ -39,8 +47,10 @@ trait RepositoryService { self: AccountService => | ||||
|     Labels        .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     IssueComments .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     Issues        .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     PullRequests  .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     IssueId       .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     Milestones    .filter(_.byRepository(userName, repositoryName)).delete | ||||
|     WebHooks      .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] = | ||||
|     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. | ||||
|    *  | ||||
| @@ -96,34 +73,69 @@ trait RepositoryService { self: AccountService => | ||||
|    */ | ||||
|   def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { | ||||
|     (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. | ||||
|    *  | ||||
|    * @param account the account | ||||
|    * Returns the list of visible repositories for the specified user. | ||||
|    * If repositoryUserName is given then filters results by repository owner. | ||||
|    * | ||||
|    * @param loginAccount the logged in account | ||||
|    * @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 newRepositoryInfo(repository: Repository): RepositoryInfo = { | ||||
|       new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository) | ||||
|     } | ||||
|  | ||||
|     (account match { | ||||
|   def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = { | ||||
|     (loginAccount match { | ||||
|       // for Administrators | ||||
|       case Some(x) if(x.isAdmin) => Query(Repositories) | ||||
|       // for Normal Users | ||||
|       case Some(x) if(!x.isAdmin) => | ||||
|         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 | ||||
|       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 = | ||||
|     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. | ||||
|    *  | ||||
| @@ -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 { | ||||
|  | ||||
|   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]) | ||||
|  | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| package service | ||||
|  | ||||
| import model._ | ||||
| import service.SystemSettingsService.SystemSettings | ||||
|  | ||||
| /** | ||||
|  * This service is used for a view helper mainly. | ||||
| @@ -10,6 +11,11 @@ import model._ | ||||
|  */ | ||||
| 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] = { | ||||
|     context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ | ||||
|       new IssuesService {}.getIssue(userName, repositoryName, issueId) | ||||
|   | ||||
| @@ -1,40 +1,152 @@ | ||||
| package service | ||||
|  | ||||
| import util.Directory._ | ||||
| import SystemSettingsService._ | ||||
|  | ||||
| trait SystemSettingsService { | ||||
|  | ||||
|   def saveSystemSettings(settings: SystemSettings): Unit = { | ||||
|     val props = new java.util.Properties() | ||||
|     props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) | ||||
|     props.store(new java.io.FileOutputStream(GitBucketConf), null) | ||||
|   } | ||||
|  | ||||
|  | ||||
|   def loadSystemSettings(): SystemSettings = { | ||||
|     val props = new java.util.Properties() | ||||
|     if(GitBucketConf.exists){ | ||||
|       props.load(new java.io.FileInputStream(GitBucketConf)) | ||||
|     } | ||||
|     SystemSettings(getBoolean(props, "allow_account_registration")) | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| object SystemSettingsService { | ||||
|  | ||||
|   case class SystemSettings(allowAccountRegistration: Boolean) | ||||
|  | ||||
|   private val AllowAccountRegistration = "allow_account_registration" | ||||
|  | ||||
|   private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = { | ||||
|     val value = props.getProperty(key) | ||||
|     if(value == null || value.isEmpty){ | ||||
|       default | ||||
|     } else { | ||||
|       value.toBoolean | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| package service | ||||
|  | ||||
| import util.Directory._ | ||||
| import util.ControlUtil._ | ||||
| import SystemSettingsService._ | ||||
|  | ||||
| trait SystemSettingsService { | ||||
|  | ||||
|   def saveSystemSettings(settings: SystemSettings): Unit = { | ||||
|     defining(new java.util.Properties()){ props => | ||||
|       props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) | ||||
|       props.setProperty(Gravatar, settings.gravatar.toString) | ||||
|       props.setProperty(Notification, settings.notification.toString) | ||||
|       if(settings.notification) { | ||||
|         settings.smtp.foreach { smtp => | ||||
|           props.setProperty(SmtpHost, smtp.host) | ||||
|           smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) | ||||
|           smtp.user.foreach(props.setProperty(SmtpUser, _)) | ||||
|           smtp.password.foreach(props.setProperty(SmtpPassword, _)) | ||||
|           smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) | ||||
|           smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) | ||||
|           smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) | ||||
|         } | ||||
|       } | ||||
|       props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) | ||||
|       if(settings.ldapAuthentication){ | ||||
|         settings.ldap.map { ldap => | ||||
|           props.setProperty(LdapHost, ldap.host) | ||||
|           ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) | ||||
|           ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) | ||||
|           ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) | ||||
|           props.setProperty(LdapBaseDN, ldap.baseDN) | ||||
|           props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) | ||||
|           props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) | ||||
|         } | ||||
|       } | ||||
|       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 | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										143
									
								
								src/main/scala/service/WebHookService.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/main/scala/service/WebHookService.scala
									
									
									
									
									
										Normal 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) | ||||
|  | ||||
| } | ||||
| @@ -4,11 +4,11 @@ import java.io.File | ||||
| import java.util.Date | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.apache.commons.io.FileUtils | ||||
| import util.JGitUtil.DiffInfo | ||||
| import util.{Directory, JGitUtil} | ||||
| import org.eclipse.jgit.lib.RepositoryBuilder | ||||
| import util.{StringUtil, Directory, JGitUtil, LockUtil} | ||||
| import util.ControlUtil._ | ||||
| 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 { | ||||
|    | ||||
| @@ -19,8 +19,9 @@ object WikiService { | ||||
|    * @param content the page content | ||||
|    * @param committer the last committer | ||||
|    * @param time the last modified time | ||||
|    * @param id the latest commit id | ||||
|    */ | ||||
|   case class WikiPageInfo(name: String, content: String, committer: String, time: Date) | ||||
|   case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) | ||||
|    | ||||
|   /** | ||||
|    * The model for wiki page history. | ||||
| @@ -32,65 +33,35 @@ object WikiService { | ||||
|    */ | ||||
|   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 { | ||||
|   import WikiService._ | ||||
|  | ||||
|   def createWikiRepository(owner: model.Account, repository: String): Unit = { | ||||
|     lock(owner.userName, repository){ | ||||
|       val dir = Directory.getWikiRepositoryDir(owner.userName, repository) | ||||
|       if(!dir.exists){ | ||||
|         try { | ||||
|           JGitUtil.initRepository(dir) | ||||
|           saveWikiPage(owner.userName, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", owner, "Initial Commit") | ||||
|         } finally { | ||||
|           // once delete cloned repository because initial cloned repository does not have 'branch.master.merge' | ||||
|           FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository)) | ||||
|   def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = | ||||
|     LockUtil.lock(s"${owner}/${repository}/wiki"){ | ||||
|       defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => | ||||
|         if(!dir.exists){ | ||||
|           try { | ||||
|             JGitUtil.initRepository(dir) | ||||
|             saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) | ||||
|           } finally { | ||||
|             // once delete cloned repository because initial cloned repository does not have 'branch.master.merge' | ||||
|             FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository)) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Returns the wiki page. | ||||
|    */ | ||||
|   def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { | ||||
|     JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => | ||||
|       try { | ||||
|     using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => | ||||
|       optionIf(!JGitUtil.isEmpty(git)){ | ||||
|         JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => | ||||
|           WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time) | ||||
|           WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId) | ||||
|         } | ||||
|       } 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. | ||||
|    */ | ||||
|   def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = { | ||||
|     JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git => | ||||
|       try { | ||||
|   def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = | ||||
|     using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => | ||||
|       optionIf(!JGitUtil.isEmpty(git)){ | ||||
|         val index = path.lastIndexOf('/') | ||||
|         val parentPath = if(index < 0) "."  else path.substring(0, index) | ||||
|         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 => | ||||
|           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. | ||||
|    */ | ||||
|   def getWikiPageList(owner: String, repository: String): List[String] = { | ||||
|     JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".") | ||||
|       .filter(_.name.endsWith(".md")) | ||||
|       .map(_.name.replaceFirst("\\.md$", "")) | ||||
|       .sortBy(x => x) | ||||
|     using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => | ||||
|       JGitUtil.getFileList(git, "master", ".") | ||||
|         .filter(_.name.endsWith(".md")) | ||||
|         .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. | ||||
|    */ | ||||
|   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){ | ||||
|       // clone working copy | ||||
|       val workDir = Directory.getWikiWorkDir(owner, repository) | ||||
|       cloneOrPullWorkingCopy(workDir, owner, repository) | ||||
|     LockUtil.lock(s"${owner}/${repository}/wiki"){ | ||||
|       defining(Directory.getWikiWorkDir(owner, repository)){ workDir => | ||||
|         // clone working copy | ||||
|         cloneOrPullWorkingCopy(workDir, owner, repository) | ||||
|  | ||||
|       // write as file | ||||
|       JGitUtil.withGit(workDir){ git => | ||||
|         val file = new File(workDir, newPageName + ".md") | ||||
|         val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ | ||||
|           FileUtils.writeStringToFile(file, content, "UTF-8") | ||||
|           git.add.addFilepattern(file.getName).call | ||||
|           true | ||||
|         } else { | ||||
|           false | ||||
|         } | ||||
|         // write as file | ||||
|         using(Git.open(workDir)){ git => | ||||
|           defining(new File(workDir, newPageName + ".md")){ file => | ||||
|             // new page | ||||
|             val created = !file.exists | ||||
|  | ||||
|         // delete file | ||||
|         val deleted = if(currentPageName != "" && currentPageName != newPageName){ | ||||
|           git.rm.addFilepattern(currentPageName + ".md").call | ||||
|           true | ||||
|         } else { | ||||
|           false | ||||
|         } | ||||
|             // created or updated | ||||
|             val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ | ||||
|               FileUtils.writeStringToFile(file, content, "UTF-8") | ||||
|               git.add.addFilepattern(file.getName).call | ||||
|             } | ||||
|  | ||||
|         // commit and push | ||||
|         if(added || deleted){ | ||||
|           git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call | ||||
|           git.push.call | ||||
|             // delete file | ||||
|             val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){ | ||||
|               git.rm.addFilepattern(currentPageName + ".md").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. | ||||
|    */ | ||||
|   def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = { | ||||
|     lock(owner, repository){ | ||||
|       // clone working copy | ||||
|       val workDir = Directory.getWikiWorkDir(owner, repository) | ||||
|       cloneOrPullWorkingCopy(workDir, owner, repository) | ||||
|   def deleteWikiPage(owner: String, repository: String, pageName: String, | ||||
|                      committer: String, mailAddress: String, message: String): Unit = { | ||||
|     LockUtil.lock(s"${owner}/${repository}/wiki"){ | ||||
|       defining(Directory.getWikiWorkDir(owner, repository)){ workDir => | ||||
|         // clone working copy | ||||
|         cloneOrPullWorkingCopy(workDir, owner, repository) | ||||
|  | ||||
|       // delete file | ||||
|       new File(workDir, pageName + ".md").delete | ||||
|      | ||||
|       JGitUtil.withGit(workDir){ git => | ||||
|         git.rm.addFilepattern(pageName + ".md").call | ||||
|      | ||||
|         // commit and push | ||||
|         // TODO committer's mail address | ||||
|         git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call | ||||
|         git.push.call | ||||
|         // delete file | ||||
|         new File(workDir, pageName + ".md").delete | ||||
|  | ||||
|         using(Git.open(workDir)){ git => | ||||
|           git.rm.addFilepattern(pageName + ".md").call | ||||
|  | ||||
|           // commit and push | ||||
|           git.commit.setCommitter(committer, mailAddress).setMessage(message).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 = { | ||||
|     if(!workDir.exists){ | ||||
|       Git.cloneRepository | ||||
|         .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString) | ||||
|         .setDirectory(workDir) | ||||
|         .call | ||||
|     } else { | ||||
|       Git.open(workDir).pull.call | ||||
|         .getRepository | ||||
|         .close | ||||
|     } else using(Git.open(workDir)){ git => | ||||
|       git.pull.call | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| package servlet | ||||
|  | ||||
| import java.io.File | ||||
| import java.sql.Connection | ||||
| import java.sql.{DriverManager, Connection} | ||||
| import org.apache.commons.io.FileUtils | ||||
| import javax.servlet.ServletContextEvent | ||||
| import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent} | ||||
| import org.apache.commons.io.IOUtils | ||||
| import org.slf4j.LoggerFactory | ||||
| import util.Directory | ||||
| import util.Directory._ | ||||
| import util.ControlUtil._ | ||||
| import org.eclipse.jgit.api.Git | ||||
|  | ||||
| object AutoUpdate { | ||||
|    | ||||
| @@ -26,15 +28,14 @@ object AutoUpdate { | ||||
|      */ | ||||
|     def update(conn: Connection): Unit = { | ||||
|       val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" | ||||
|       val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath) | ||||
|       if(in != null){ | ||||
|         val sql = IOUtils.toString(in, "UTF-8") | ||||
|         val stmt = conn.createStatement() | ||||
|         try { | ||||
|           logger.debug(sqlPath + "=" + sql) | ||||
|           stmt.executeUpdate(sql) | ||||
|         } finally { | ||||
|           stmt.close() | ||||
|  | ||||
|       using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => | ||||
|         if(in != null){ | ||||
|           val sql = IOUtils.toString(in, "UTF-8") | ||||
|           using(conn.createStatement()){ stmt => | ||||
|             logger.debug(sqlPath + "=" + sql) | ||||
|             stmt.executeUpdate(sql) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -49,26 +50,32 @@ object AutoUpdate { | ||||
|    * The history of versions. A head of this sequence is the current BitBucket version. | ||||
|    */ | ||||
|   val versions = Seq( | ||||
|     Version(1, 7), | ||||
|     Version(1, 6), | ||||
|     Version(1, 5), | ||||
|     Version(1, 4), | ||||
|     new Version(1, 3){ | ||||
|       override def update(conn: Connection): Unit = { | ||||
|         super.update(conn) | ||||
|         // Fix wiki repository configuration | ||||
|         val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY") | ||||
|         while(rs.next){ | ||||
|           val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")) | ||||
|           val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository | ||||
|           val config = repository.getConfig | ||||
|           if(!config.getBoolean("http", "receivepack", false)){ | ||||
|             config.setBoolean("http", null, "receivepack", true) | ||||
|             config.save | ||||
|         using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs => | ||||
|           while(rs.next){ | ||||
|             using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => | ||||
|               defining(git.getRepository.getConfig){ config => | ||||
|                 if(!config.getBoolean("http", "receivepack", false)){ | ||||
|                   config.setBoolean("http", null, "receivepack", true) | ||||
|                   config.save | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           repository.close | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     Version(1, 2), | ||||
|     Version(1, 1), | ||||
|     Version(1, 0) | ||||
|     Version(1, 0), | ||||
|     Version(0, 0) | ||||
|   ) | ||||
|    | ||||
|   /** | ||||
| @@ -79,7 +86,7 @@ object AutoUpdate { | ||||
|   /** | ||||
|    * 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. | ||||
| @@ -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._ | ||||
|   private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) | ||||
|    | ||||
|   override def contextInitialized(event: ServletContextEvent): Unit = { | ||||
|     super.contextInitialized(event) | ||||
|     logger.debug("H2 started") | ||||
|      | ||||
|     org.h2.Driver.load() | ||||
|     event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}") | ||||
|  | ||||
|     logger.debug("Start schema update") | ||||
|     val conn = getConnection() | ||||
|     try { | ||||
|       val currentVersion = getCurrentVersion() | ||||
|       if(currentVersion == headVersion){ | ||||
|         logger.debug("No update") | ||||
|       } else { | ||||
|         versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) | ||||
|         FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") | ||||
|         conn.commit() | ||||
|         logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString) | ||||
|       } | ||||
|     } catch { | ||||
|       case ex: Throwable => { | ||||
|         logger.error("Failed to schema update", ex) | ||||
|         conn.rollback() | ||||
|     defining(getConnection(event.getServletContext)){ conn => | ||||
|       try { | ||||
|         defining(getCurrentVersion()){ currentVersion => | ||||
|           if(currentVersion == headVersion){ | ||||
|             logger.debug("No update") | ||||
|           } else if(!versions.contains(currentVersion)){ | ||||
|             logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") | ||||
|           } else { | ||||
|             versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) | ||||
|             FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") | ||||
|             conn.commit() | ||||
|             logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") | ||||
|           } | ||||
|         } | ||||
|       } catch { | ||||
|         case ex: Throwable => { | ||||
|           logger.error("Failed to schema update", ex) | ||||
|           ex.printStackTrace() | ||||
|           conn.rollback() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     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")) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -2,14 +2,16 @@ package servlet | ||||
|  | ||||
| import javax.servlet._ | ||||
| import javax.servlet.http._ | ||||
| import util.StringUtil._ | ||||
| import service.{AccountService, RepositoryService} | ||||
| import service.{SystemSettingsService, AccountService, RepositoryService} | ||||
| import org.slf4j.LoggerFactory | ||||
| import util.Implicits._ | ||||
| import util.ControlUtil._ | ||||
| import util.Keys | ||||
|  | ||||
| /** | ||||
|  * 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]) | ||||
|  | ||||
| @@ -26,28 +28,30 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       val repositoryOwner = paths(2) | ||||
|       val repositoryName  = paths(3).replaceFirst("\\.git$", "") | ||||
|  | ||||
|       getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match { | ||||
|         case Some(repository) => { | ||||
|           if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){ | ||||
|             chain.doFilter(req, wrappedResponse) | ||||
|           } else { | ||||
|             request.getHeader("Authorization") match { | ||||
|               case null => requireAuth(response) | ||||
|               case auth => decodeAuthHeader(auth).split(":") match { | ||||
|                 case Array(username, password) if(isWritableUser(username, password, repository)) => { | ||||
|                   request.setAttribute("USER_NAME", username) | ||||
|                   chain.doFilter(req, wrappedResponse) | ||||
|       defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) => | ||||
|         getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { | ||||
|           case Some(repository) => { | ||||
|             if(!request.getRequestURI.endsWith("/git-receive-pack") && | ||||
|               !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){ | ||||
|               chain.doFilter(req, wrappedResponse) | ||||
|             } else { | ||||
|               request.getHeader("Authorization") match { | ||||
|                 case null => requireAuth(response) | ||||
|                 case auth => decodeAuthHeader(auth).split(":") match { | ||||
|                   case Array(username, password) if(isWritableUser(username, password, repository)) => { | ||||
|                     request.setAttribute(Keys.Request.UserName, 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 { | ||||
|       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 = { | ||||
|     getAccountByUserName(username).map { account => | ||||
|       account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account)) | ||||
|     } getOrElse false | ||||
|   } | ||||
|    | ||||
|   private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = | ||||
|     authenticate(loadSystemSettings(), username, password) match { | ||||
|       case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account)) | ||||
|       case None => false | ||||
|     } | ||||
|  | ||||
|   private def requireAuth(response: HttpServletResponse): Unit = { | ||||
|     response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") | ||||
|     response.sendError(HttpServletResponse.SC_UNAUTHORIZED) | ||||
|   | ||||
| @@ -9,8 +9,13 @@ import org.slf4j.LoggerFactory | ||||
| import javax.servlet.ServletConfig | ||||
| import javax.servlet.ServletContext | ||||
| import javax.servlet.http.HttpServletRequest | ||||
| import util.{JGitUtil, Directory} | ||||
| import util.{Keys, JGitUtil, Directory} | ||||
| import util.ControlUtil._ | ||||
| import util.Implicits._ | ||||
| import service._ | ||||
| import WebHookService._ | ||||
| import org.eclipse.jgit.api.Git | ||||
| import util.JGitUtil.CommitInfo | ||||
|  | ||||
| /** | ||||
|  * Provides Git repository via HTTP. | ||||
| @@ -24,7 +29,7 @@ class GitRepositoryServlet extends GitServlet { | ||||
|    | ||||
|   override def init(config: ServletConfig): Unit = { | ||||
|     setReceivePackFactory(new GitBucketReceivePackFactory()) | ||||
|      | ||||
|  | ||||
|     // TODO are there any other ways...? | ||||
|     super.init(new ServletConfig(){ | ||||
|       def getInitParameter(name: String): String = name match { | ||||
| @@ -33,12 +38,14 @@ class GitRepositoryServlet extends GitServlet { | ||||
|         case name => config.getInitParameter(name) | ||||
|       } | ||||
|       def getInitParameterNames(): java.util.Enumeration[String] = { | ||||
|         config.getInitParameterNames | ||||
|          config.getInitParameterNames | ||||
|       } | ||||
|        | ||||
|  | ||||
|       def getServletContext(): ServletContext = config.getServletContext | ||||
|       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 = { | ||||
|     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("userName:" + userName) | ||||
|  | ||||
|     val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|     val owner      = paths(2) | ||||
|     val repository = paths(3).replaceFirst("\\.git$", "") | ||||
|      | ||||
|     logger.debug("repository:" + owner + "/" + repository) | ||||
|     defining(request.paths){ paths => | ||||
|       val owner      = paths(1) | ||||
|       val repository = paths(2).replaceFirst("\\.git$", "") | ||||
|       val baseURL    = request.getRequestURL.toString.replaceFirst("/git/.*", "") | ||||
|  | ||||
|     receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName)) | ||||
|     receivePack | ||||
|       logger.debug("repository:" + owner + "/" + repository) | ||||
|       logger.debug("baseURL:" + baseURL) | ||||
|  | ||||
|       receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL)) | ||||
|       receivePack | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| import scala.collection.JavaConverters._ | ||||
|  | ||||
| class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook | ||||
|   with RepositoryService with AccountService with IssuesService with ActivityService { | ||||
| class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook | ||||
|   with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService { | ||||
|    | ||||
|   private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) | ||||
|    | ||||
|   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 => | ||||
|         val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) | ||||
|         val refName = command.getRefName.split("/") | ||||
|          | ||||
|         // apply issue comment | ||||
|         val newCommits = commits.flatMap { commit => | ||||
|           if(!existsCommitId(owner, repository, commit.id)){ | ||||
|             insertCommitId(owner, repository, commit.id) | ||||
|             "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData => | ||||
|               val issueId = matchData.group(2) | ||||
|               if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){ | ||||
|                 createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit") | ||||
|               } | ||||
|         val branchName = refName.drop(2).mkString("/") | ||||
|  | ||||
|         // Extract new commit and apply issue comment | ||||
|         val newCommits = if(commits.size > 1000){ | ||||
|           val existIds = getAllCommitIds(owner, repository) | ||||
|           commits.flatMap { commit => | ||||
|             optionIf(!existIds.contains(commit.id)){ | ||||
|               createIssueComment(commit) | ||||
|               Some(commit) | ||||
|             } | ||||
|             Some(commit) | ||||
|           } else None | ||||
|         }.toList | ||||
|          | ||||
|           } | ||||
|         } else { | ||||
|           commits.flatMap { commit => | ||||
|             optionIf(!existsCommitId(owner, repository, commit.id)){ | ||||
|               createIssueComment(commit) | ||||
|               Some(commit) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // batch insert all new commit id | ||||
|         insertAllCommitIds(owner, repository, newCommits.map(_.id)) | ||||
|  | ||||
|         // record activity | ||||
|         if(refName(1) == "heads"){ | ||||
|           command.getType match { | ||||
|             case ReceiveCommand.Type.CREATE => { | ||||
|               recordCreateBranchActivity(owner, repository, userName, refName(2)) | ||||
|               recordPushActivity(owner, repository, userName, refName(2), newCommits) | ||||
|               recordCreateBranchActivity(owner, repository, userName, branchName) | ||||
|               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 _ => | ||||
|           } | ||||
|         } else if(refName(1) == "tags"){ | ||||
|           command.getType match { | ||||
|             case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits) | ||||
|             case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits) | ||||
|             case _ => | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // 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. | ||||
|     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") | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,15 @@ | ||||
| package servlet | ||||
|  | ||||
| import util.FileUploadUtil | ||||
| import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} | ||||
| import app.FileUploadControllerBase | ||||
|  | ||||
| /** | ||||
|  * 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 sessionDestroyed(se: HttpSessionEvent): Unit = { | ||||
|     FileUploadUtil.removeTemporaryFiles()(se.getSession) | ||||
|   } | ||||
|   def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package servlet | ||||
| import javax.servlet._ | ||||
| import org.slf4j.LoggerFactory | ||||
| import javax.servlet.http.HttpServletRequest | ||||
| import scala.slick.session.Database | ||||
|  | ||||
| /** | ||||
|  * Controls the transaction with the open session in view pattern. | ||||
| @@ -21,15 +20,19 @@ class TransactionFilter extends Filter { | ||||
|       // assets don't need transaction | ||||
|       chain.doFilter(req, res) | ||||
|     } else { | ||||
|       val context = req.getServletContext | ||||
|       Database.forURL(context.getInitParameter("db.url"), | ||||
|           context.getInitParameter("db.user"), | ||||
|           context.getInitParameter("db.password")) withTransaction { | ||||
|         logger.debug("TODO begin transaction") | ||||
|       Database(req.getServletContext) withTransaction { | ||||
|         logger.debug("begin transaction") | ||||
|         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")) | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,8 @@ package util | ||||
| import app.ControllerBase | ||||
| import service._ | ||||
| import RepositoryService.RepositoryInfo | ||||
| import util.Implicits._ | ||||
| import util.ControlUtil._ | ||||
|  | ||||
| /** | ||||
|  * Allows only oneself and administrators. | ||||
| @@ -13,11 +15,12 @@ trait OneselfAuthenticator { self: ControllerBase => | ||||
|  | ||||
|   private def authenticate(action: => Any) = { | ||||
|     { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       context.loginAccount match { | ||||
|         case Some(x) if(x.isAdmin) => action | ||||
|         case Some(x) if(paths(1) == x.userName) => action | ||||
|         case _ => Unauthorized() | ||||
|       defining(request.paths){ paths => | ||||
|         context.loginAccount match { | ||||
|           case Some(x) if(x.isAdmin) => action | ||||
|           case Some(x) if(paths(0) == x.userName) => action | ||||
|           case _ => Unauthorized() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -32,14 +35,15 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService => | ||||
|  | ||||
|   private def authenticate(action: (RepositoryInfo) => Any) = { | ||||
|     { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       getRepository(paths(1), paths(2), baseUrl).map { repository => | ||||
|         context.loginAccount match { | ||||
|           case Some(x) if(x.isAdmin) => action(repository) | ||||
|           case Some(x) if(repository.owner == x.userName) => action(repository) | ||||
|           case _ => Unauthorized() | ||||
|         } | ||||
|       } getOrElse NotFound() | ||||
|       defining(request.paths){ paths => | ||||
|         getRepository(paths(0), paths(1), baseUrl).map { repository => | ||||
|           context.loginAccount match { | ||||
|             case Some(x) if(x.isAdmin) => action(repository) | ||||
|             case Some(x) if(repository.owner == x.userName) => action(repository) | ||||
|             case _ => Unauthorized() | ||||
|           } | ||||
|         } getOrElse NotFound() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -87,15 +91,16 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService = | ||||
|  | ||||
|   private def authenticate(action: (RepositoryInfo) => Any) = { | ||||
|     { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       getRepository(paths(1), paths(2), baseUrl).map { repository => | ||||
|         context.loginAccount match { | ||||
|           case Some(x) if(x.isAdmin) => action(repository) | ||||
|           case Some(x) if(paths(1) == x.userName) => action(repository) | ||||
|           case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) | ||||
|           case _ => Unauthorized() | ||||
|         } | ||||
|       } getOrElse NotFound() | ||||
|       defining(request.paths){ paths => | ||||
|         getRepository(paths(0), paths(1), baseUrl).map { repository => | ||||
|           context.loginAccount match { | ||||
|             case Some(x) if(x.isAdmin) => action(repository) | ||||
|             case Some(x) if(paths(0) == x.userName) => action(repository) | ||||
|             case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) | ||||
|             case _ => Unauthorized() | ||||
|           } | ||||
|         } getOrElse NotFound() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -109,19 +114,20 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => | ||||
|  | ||||
|   private def authenticate(action: (RepositoryInfo) => Any) = { | ||||
|     { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       getRepository(paths(1), paths(2), baseUrl).map { repository => | ||||
|         if(!repository.repository.isPrivate){ | ||||
|           action(repository) | ||||
|         } else { | ||||
|           context.loginAccount match { | ||||
|             case Some(x) if(x.isAdmin) => action(repository) | ||||
|             case Some(x) if(paths(1) == x.userName) => action(repository) | ||||
|             case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) | ||||
|             case _ => Unauthorized() | ||||
|       defining(request.paths){ paths => | ||||
|         getRepository(paths(0), paths(1), baseUrl).map { repository => | ||||
|           if(!repository.repository.isPrivate){ | ||||
|             action(repository) | ||||
|           } else { | ||||
|             context.loginAccount match { | ||||
|               case Some(x) if(x.isAdmin) => action(repository) | ||||
|               case Some(x) if(paths(0) == x.userName) => action(repository) | ||||
|               case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) | ||||
|               case _ => Unauthorized() | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } getOrElse NotFound() | ||||
|         } getOrElse NotFound() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -135,16 +141,17 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService = | ||||
|  | ||||
|   private def authenticate(action: (RepositoryInfo) => Any) = { | ||||
|     { | ||||
|       val paths = request.getRequestURI.substring(request.getContextPath.length).split("/") | ||||
|       getRepository(paths(1), paths(2), baseUrl).map { repository => | ||||
|         context.loginAccount match { | ||||
|           case Some(x) if(x.isAdmin) => 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(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository) | ||||
|           case _ => Unauthorized() | ||||
|         } | ||||
|       } getOrElse NotFound() | ||||
|       defining(request.paths){ paths => | ||||
|         getRepository(paths(0), paths(1), baseUrl).map { repository => | ||||
|           context.loginAccount match { | ||||
|             case Some(x) if(x.isAdmin) => action(repository) | ||||
|             case Some(x) if(!repository.repository.isPrivate) => action(repository) | ||||
|             case Some(x) if(paths(0) == x.userName) => action(repository) | ||||
|             case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) | ||||
|             case _ => Unauthorized() | ||||
|           } | ||||
|         } getOrElse NotFound() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										48
									
								
								src/main/scala/util/ControlUtil.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/main/scala/util/ControlUtil.scala
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -1,34 +1,38 @@ | ||||
| package util | ||||
|  | ||||
| import java.io.File | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.lib.Ref | ||||
| import util.ControlUtil._ | ||||
|  | ||||
| /** | ||||
|  * Provides directories used by GitBucket. | ||||
|  */ | ||||
| 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 RepositoryHome = s"${GitBucketHome}/repositories" | ||||
|  | ||||
|   val DatabaseHome = s"${GitBucketHome}/data" | ||||
|    | ||||
|   /** | ||||
|    * Repository names of the specified user. | ||||
|    */ | ||||
|   def getRepositories(owner: String): List[String] = { | ||||
|     val dir = new File(s"${RepositoryHome}/${owner}") | ||||
|     if(dir.exists){ | ||||
|       dir.listFiles.filter { file => | ||||
|         file.isDirectory && !file.getName.endsWith(".wiki.git")  | ||||
|       }.map(_.getName.replaceFirst("\\.git$", "")).toList | ||||
|     } else { | ||||
|       Nil | ||||
|   def getRepositories(owner: String): List[String] = | ||||
|     defining(new File(s"${RepositoryHome}/${owner}")){ dir => | ||||
|       if(dir.exists){ | ||||
|         dir.listFiles.filter { file => | ||||
|           file.isDirectory && !file.getName.endsWith(".wiki.git") | ||||
|         }.map(_.getName.replaceFirst("\\.git$", "")).toList | ||||
|       } else { | ||||
|         Nil | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Substance directory of the repository. | ||||
|    */ | ||||
|   | ||||
| @@ -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 | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,22 +1,31 @@ | ||||
| package util | ||||
|  | ||||
| import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils} | ||||
| import org.apache.commons.io.{IOUtils, FileUtils} | ||||
| import java.net.URLConnection | ||||
| import java.io.File | ||||
| import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream} | ||||
| import util.ControlUtil._ | ||||
|  | ||||
| object FileUtil { | ||||
|    | ||||
|   def getMimeType(name: String): String = { | ||||
|     val fileNameMap = URLConnection.getFileNameMap() | ||||
|     val mimeType = fileNameMap.getContentTypeFor(name) | ||||
|     if(mimeType == null){ | ||||
|       "application/octeat-stream" | ||||
|     } else { | ||||
|       mimeType | ||||
|   def getMimeType(name: String): String = | ||||
|     defining(URLConnection.getFileNameMap()){ fileNameMap => | ||||
|       fileNameMap.getContentTypeFor(name) match { | ||||
|         case null     => "application/octet-stream" | ||||
|         case mimeType => 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 isLarge(size: Long): Boolean = (size > 1024 * 1000) | ||||
| @@ -36,21 +45,29 @@ object FileUtil { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     val out = new ZipArchiveOutputStream(dest) | ||||
|     try { | ||||
|     using(new ZipArchiveOutputStream(dest)){ out => | ||||
|       addDirectoryToZip(out, dir, dir.getName) | ||||
|     } finally { | ||||
|       IOUtils.closeQuietly(out) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getExtension(name: String): String = { | ||||
|     val index = name.lastIndexOf('.') | ||||
|     if(index >= 0){ | ||||
|       name.substring(index + 1) | ||||
|     } else { | ||||
|       "" | ||||
|     } | ||||
|   def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i => | ||||
|     if(i >= 0) path.substring(i + 1) else path | ||||
|   } | ||||
|  | ||||
| } | ||||
|   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) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package util | ||||
|  | ||||
| import scala.slick.driver.H2Driver.simple._ | ||||
| import scala.util.matching.Regex | ||||
| import javax.servlet.http.{HttpSession, HttpServletRequest} | ||||
|  | ||||
| /** | ||||
|  * Provides some usable implicit conversions. | ||||
| @@ -13,7 +13,7 @@ object Implicits { | ||||
|     def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) | ||||
|  | ||||
|     @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 { | ||||
|         case x :: xs => { | ||||
|           xs.span(condition(x, _)) match { | ||||
| @@ -22,12 +22,6 @@ object Implicits { | ||||
|         } | ||||
|         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){ | ||||
| @@ -47,6 +41,38 @@ object Implicits { | ||||
|       } | ||||
|       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) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -2,19 +2,19 @@ package util | ||||
|  | ||||
| import org.eclipse.jgit.api.Git | ||||
| import util.Directory._ | ||||
| import util.StringUtil._ | ||||
| import util.ControlUtil._ | ||||
| import scala.collection.JavaConverters._ | ||||
| import javax.servlet.ServletContext | ||||
| import org.eclipse.jgit.lib._ | ||||
| import org.eclipse.jgit.revwalk._ | ||||
| import org.eclipse.jgit.revwalk.filter._ | ||||
| import org.eclipse.jgit.treewalk._ | ||||
| import org.eclipse.jgit.treewalk.filter._ | ||||
| import org.eclipse.jgit.diff._ | ||||
| import org.eclipse.jgit.diff.DiffEntry.ChangeType | ||||
| import org.eclipse.jgit.util.io.DisabledOutputStream | ||||
| import org.eclipse.jgit.errors.MissingObjectException | ||||
| import java.util.Date | ||||
| import org.eclipse.jgit.api.errors.NoHeadException | ||||
| import service.RepositoryService | ||||
|  | ||||
| /** | ||||
|  * Provides complex JGit operations. | ||||
| @@ -71,29 +71,17 @@ object JGitUtil { | ||||
|         rev.getFullMessage, | ||||
|         rev.getParents().map(_.name).toList) | ||||
|  | ||||
|     val summary = { | ||||
|       val i = fullMessage.trim.indexOf("\n") | ||||
|       val firstLine = if(i >= 0){ | ||||
|         fullMessage.trim.substring(0, i).trim | ||||
|       } else { | ||||
|         fullMessage | ||||
|       } | ||||
|       if(firstLine.length > shortMessage.length){ | ||||
|         shortMessage | ||||
|       } else { | ||||
|         firstLine | ||||
|     val summary = defining(fullMessage.trim.indexOf("\n")){ i => | ||||
|       defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine => | ||||
|         if(firstLine.length > shortMessage.length) shortMessage else firstLine | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     val description = { | ||||
|       val i = fullMessage.trim.indexOf("\n") | ||||
|       if(i >= 0){ | ||||
|     val description = defining(fullMessage.trim.indexOf("\n")){ i => | ||||
|       optionIf(i >= 0){ | ||||
|         Some(fullMessage.trim.substring(i).trim) | ||||
|       } else { | ||||
|         None | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   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) | ||||
|  | ||||
|   /** | ||||
|    * Use this method to use the Git object. | ||||
|    * 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. | ||||
|    * Returns RevCommit from the commit or tag id. | ||||
|    *  | ||||
|    * @param git the Git object | ||||
|    * @param commitId the ObjectId of the commit | ||||
|    * @return the RevCommit for the specified commit | ||||
|    * @param objectId the ObjectId of the commit or tag | ||||
|    * @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 revCommit = revWalk.parseCommit(commitId) | ||||
|     val revCommit = revWalk.parseAny(objectId) match { | ||||
|       case r: RevTag => revWalk.parseCommit(r.getObject) | ||||
|       case _         => revWalk.parseCommit(objectId) | ||||
|     } | ||||
|     revWalk.dispose | ||||
|     revCommit | ||||
|   } | ||||
| @@ -151,15 +124,10 @@ object JGitUtil { | ||||
|    * Returns the repository information. It contains branch names and tag names. | ||||
|    */ | ||||
|   def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { | ||||
|     withGit(getRepositoryDir(owner, repository)){ git => | ||||
|     using(Git.open(getRepositoryDir(owner, repository))){ git => | ||||
|       try { | ||||
|         // get commit count | ||||
|         val i = git.log.all.call.iterator | ||||
|         var commitCount = 0 | ||||
|         while(i.hasNext && commitCount <= 1000){ | ||||
|           i.next | ||||
|           commitCount = commitCount + 1 | ||||
|         } | ||||
|         val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum | ||||
|  | ||||
|         RepositoryInfo( | ||||
|           owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", | ||||
| @@ -193,45 +161,43 @@ object JGitUtil { | ||||
|    * @return HTML of the file list | ||||
|    */ | ||||
|   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)] | ||||
|      | ||||
|     while (treeWalk.next()) { | ||||
|       list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) | ||||
|  | ||||
|     using(new RevWalk(git.getRepository)){ revWalk => | ||||
|       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) | ||||
|     list.map { case (objectId, fileMode, path, name) => | ||||
| @@ -261,7 +227,7 @@ object JGitUtil { | ||||
|    * @param page the page number (1-) | ||||
|    * @param limit the number of commit info per page. 0 (default) means unlimited. | ||||
|    * @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)] = { | ||||
|     val fixedPage = if(page <= 0) 1 else page | ||||
| @@ -276,27 +242,48 @@ object JGitUtil { | ||||
|         case _ => (logs, i.hasNext) | ||||
|       } | ||||
|      | ||||
|     val revWalk = new RevWalk(git.getRepository) | ||||
|     val objectId = git.getRepository.resolve(revision) | ||||
|     if(objectId == null){ | ||||
|       Left(s"${revision} can't be resolved.") | ||||
|     } else { | ||||
|       revWalk.markStart(revWalk.parseCommit(objectId)) | ||||
|       if(path.nonEmpty){ | ||||
|         revWalk.setRevFilter(new RevFilter(){ | ||||
|           def include(walk: RevWalk, commit: RevCommit): Boolean = { | ||||
|             getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty | ||||
|     using(new RevWalk(git.getRepository)){ revWalk => | ||||
|       defining(git.getRepository.resolve(revision)){ objectId => | ||||
|         if(objectId == null){ | ||||
|           Left(s"${revision} can't be resolved.") | ||||
|         } else { | ||||
|           revWalk.markStart(revWalk.parseCommit(objectId)) | ||||
|           if(path.nonEmpty){ | ||||
|             revWalk.setRevFilter(new RevFilter(){ | ||||
|               def include(walk: RevWalk, commit: RevCommit): Boolean = { | ||||
|                 getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty | ||||
|               } | ||||
|               override def clone(): RevFilter = this | ||||
|             }) | ||||
|           } | ||||
|           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. | ||||
| @@ -306,30 +293,9 @@ object JGitUtil { | ||||
|    * @param to the to revision | ||||
|    * @return the commit list | ||||
|    */ | ||||
|   def getCommitLog(git: Git, from: String, to: String): 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(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 | ||||
|   } | ||||
|    | ||||
|   // TODO swap parameters 'from' and 'to'!? | ||||
|   def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = | ||||
|     getCommitLogs(git, to)(_.getName == from) | ||||
|    | ||||
|   /** | ||||
|    * Returns the latest RevCommit of the specified path. | ||||
| @@ -351,51 +317,11 @@ object JGitUtil { | ||||
|    * @return the list of latest commit | ||||
|    */ | ||||
|   def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = { | ||||
|  | ||||
|     val map = new scala.collection.mutable.HashMap[String, RevCommit] | ||||
|  | ||||
|     val revWalk = new RevWalk(git.getRepository) | ||||
|     revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision))) | ||||
|     //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 | ||||
|     val start = getRevCommitFromId(git, git.getRepository.resolve(revision)) | ||||
|     paths.map { path => | ||||
|       val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next | ||||
|       (path, commit) | ||||
|     }.toMap | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -411,126 +337,131 @@ object JGitUtil { | ||||
|     if(large == false && FileUtil.isLarge(loader.getSize)){ | ||||
|       None | ||||
|     } else { | ||||
|       val db = git.getRepository.getObjectDatabase | ||||
|       try { | ||||
|       using(git.getRepository.getObjectDatabase){ db => | ||||
|         Some(db.open(id).getBytes) | ||||
|       } finally { | ||||
|         db.close | ||||
|       } | ||||
|     } | ||||
|   } catch { | ||||
|     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 | ||||
|     def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = | ||||
|       i.hasNext match { | ||||
|         case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next) | ||||
|         case _ => logs | ||||
|       } | ||||
|      | ||||
|     val revWalk = new RevWalk(git.getRepository) | ||||
|     revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) | ||||
|      | ||||
|     val commits = getCommitLog(revWalk.iterator, Nil) | ||||
|     revWalk.release | ||||
|      | ||||
|     val revCommit = commits(0) | ||||
|      | ||||
|     if(commits.length >= 2){ | ||||
|       // not initial commit | ||||
|       val oldCommit = commits(1) | ||||
|        | ||||
|       // get diff between specified commit and its previous commit | ||||
|       val reader = git.getRepository.newObjectReader | ||||
|        | ||||
|       val oldTreeIter = new CanonicalTreeParser | ||||
|       oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}")) | ||||
|        | ||||
|       val newTreeIter = new CanonicalTreeParser | ||||
|       newTreeIter.reset(reader, git.getRepository.resolve(id + "^{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(new String(_, "UTF-8")), | ||||
|             JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8"))) | ||||
|  | ||||
|     using(new RevWalk(git.getRepository)){ revWalk => | ||||
|       revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) | ||||
|       val commits   = getCommitLog(revWalk.iterator, Nil) | ||||
|       val revCommit = commits(0) | ||||
|  | ||||
|       if(commits.length >= 2){ | ||||
|         // not initial commit | ||||
|         val oldCommit = commits(1) | ||||
|         (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) | ||||
|  | ||||
|       } else { | ||||
|         // initial commit | ||||
|         using(new TreeWalk(git.getRepository)){ treeWalk => | ||||
|           treeWalk.addTree(revCommit.getTree) | ||||
|           val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]() | ||||
|           while(treeWalk.next){ | ||||
|             buffer.append((if(!fetchContent){ | ||||
|               DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) | ||||
|             } else { | ||||
|               DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, | ||||
|                 JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) | ||||
|             })) | ||||
|           } | ||||
|           (buffer.toList, None) | ||||
|         } | ||||
|       }.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. | ||||
|    */ | ||||
|   def getBranchesOfCommit(git: Git, commitId: String): List[String] = { | ||||
|     val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) | ||||
|     try { | ||||
|       val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) | ||||
|  | ||||
|       git.getRepository.getAllRefs.entrySet.asScala.filter { e => | ||||
|         (e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) | ||||
|       }.map { e => | ||||
|         e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) | ||||
|       }.toList.sorted | ||||
|  | ||||
|     } finally { | ||||
|       walk.release | ||||
|   def getBranchesOfCommit(git: Git, commitId: String): List[String] = | ||||
|     using(new RevWalk(git.getRepository)){ revWalk => | ||||
|       defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => | ||||
|         git.getRepository.getAllRefs.entrySet.asScala.filter { e => | ||||
|           (e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) | ||||
|         }.map { e => | ||||
|           e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) | ||||
|         }.toList.sorted | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns the list of tags of the specified commit. | ||||
|    */ | ||||
|   def getTagsOfCommit(git: Git, commitId: String): List[String] = { | ||||
|     val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository) | ||||
|     try { | ||||
|       val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0")) | ||||
|  | ||||
|       git.getRepository.getAllRefs.entrySet.asScala.filter { e => | ||||
|         (e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId))) | ||||
|       }.map { e => | ||||
|         e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) | ||||
|       }.toList.sorted.reverse | ||||
|  | ||||
|     } finally { | ||||
|       walk.release | ||||
|   def getTagsOfCommit(git: Git, commitId: String): List[String] = | ||||
|     using(new RevWalk(git.getRepository)){ revWalk => | ||||
|       defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => | ||||
|         git.getRepository.getAllRefs.entrySet.asScala.filter { e => | ||||
|           (e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) | ||||
|         }.map { e => | ||||
|           e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) | ||||
|         }.toList.sorted.reverse | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def initRepository(dir: java.io.File): Unit = { | ||||
|     val repository = new RepositoryBuilder().setGitDir(dir).setBare.build | ||||
|     try { | ||||
|   def initRepository(dir: java.io.File): Unit = | ||||
|     using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository => | ||||
|       repository.create | ||||
|       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 | ||||
|   } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										72
									
								
								src/main/scala/util/Keys.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/main/scala/util/Keys.scala
									
									
									
									
									
										Normal 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}" | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										104
									
								
								src/main/scala/util/LDAPUtil.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/main/scala/util/LDAPUtil.scala
									
									
									
									
									
										Normal 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) | ||||
|       } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/main/scala/util/LockUtil.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/main/scala/util/LockUtil.scala
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										111
									
								
								src/main/scala/util/Notifier.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/main/scala/util/Notifier.scala
									
									
									
									
									
										Normal 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 = {} | ||||
| } | ||||
| @@ -1,14 +1,16 @@ | ||||
| package util | ||||
|  | ||||
| import java.net.{URLDecoder, URLEncoder} | ||||
| import org.mozilla.universalchardet.UniversalDetector | ||||
| import util.ControlUtil._ | ||||
|  | ||||
| object StringUtil { | ||||
|  | ||||
|   def sha1(value: String): String = { | ||||
|     val md = java.security.MessageDigest.getInstance("SHA-1") | ||||
|     md.update(value.getBytes) | ||||
|     md.digest.map(b => "%02x".format(b)).mkString | ||||
|   } | ||||
|   def sha1(value: String): String = | ||||
|     defining(java.security.MessageDigest.getInstance("SHA-1")){ md => | ||||
|       md.update(value.getBytes) | ||||
|       md.digest.map(b => "%02x".format(b)).mkString | ||||
|     } | ||||
|  | ||||
|   def md5(value: String): String = { | ||||
|     val md = java.security.MessageDigest.getInstance("MD5") | ||||
| @@ -20,4 +22,20 @@ object StringUtil { | ||||
|  | ||||
|   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("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) | ||||
|  | ||||
|   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 | ||||
|       } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package util | ||||
|  | ||||
| import jp.sf.amateras.scalatra.forms._ | ||||
| import scala.Some | ||||
|  | ||||
| trait Validations { | ||||
|  | ||||
| @@ -9,7 +8,7 @@ trait Validations { | ||||
|    * Constraint for the identifier such as user name, repository name or page name. | ||||
|    */ | ||||
|   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\\-_]+$")){ | ||||
|         Some(s"${name} contains invalid character.") | ||||
|       } else if(value.startsWith("_") || value.startsWith("-")){ | ||||
| @@ -26,10 +25,7 @@ trait Validations { | ||||
|    */ | ||||
|   def date(constraints: Constraint*): SingleValueType[java.util.Date] = | ||||
|     new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){ | ||||
|       def convert(value: String): java.util.Date = { | ||||
|         val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd") | ||||
|         formatter.parse(value) | ||||
|       } | ||||
|       def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,17 +12,23 @@ trait AvatarImageProvider { self: RequestCache => | ||||
|    */ | ||||
|   protected def getAvatarImageHtml(userName: String, size: Int, | ||||
|       mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = { | ||||
|     val src = getAccountByUserName(userName).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 { | ||||
|       if(mailAddress.nonEmpty){ | ||||
|       if(mailAddress.nonEmpty && getSystemSettings().gravatar){ | ||||
|         s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}""" | ||||
|       } else { | ||||
|         s"""${context.path}/${userName}/_avatar""" | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     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 { | ||||
|       Html(s"""<img src="${src}" class="avatar" style="width: ${size}px; height: ${size}px;" />""") | ||||
|     } | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| package view | ||||
|  | ||||
| import util.StringUtil | ||||
| import util.ControlUtil._ | ||||
| import util.Directory._ | ||||
| import org.parboiled.common.StringUtils | ||||
| import org.pegdown._ | ||||
| import org.pegdown.ast._ | ||||
| import org.pegdown.LinkRenderer.Rendering | ||||
| import scala.collection.JavaConverters._ | ||||
| import service.RequestCache | ||||
| import service.{RequestCache, WikiService} | ||||
|  | ||||
| object Markdown { | ||||
|  | ||||
| @@ -29,7 +31,7 @@ object Markdown { | ||||
| } | ||||
|  | ||||
| class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo, | ||||
|                           enableWikiLink: Boolean) extends LinkRenderer { | ||||
|                           enableWikiLink: Boolean) extends LinkRenderer with WikiService { | ||||
|   override def render(node: WikiLinkNode): Rendering = { | ||||
|     if(enableWikiLink){ | ||||
|       try { | ||||
| @@ -40,8 +42,14 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe | ||||
|         } else { | ||||
|           (text, text) | ||||
|         } | ||||
|  | ||||
|         val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page) | ||||
|         new Rendering(url, label) | ||||
|  | ||||
|         if(getWikiPage(repository.owner, repository.name, page).isDefined){ | ||||
|           new Rendering(url, label) | ||||
|         } else { | ||||
|           new Rendering(url, label).withAttribute("class", "absent") | ||||
|         } | ||||
|       } catch { | ||||
|         case e: java.io.UnsupportedEncodingException => throw new IllegalStateException | ||||
|       } | ||||
| @@ -52,7 +60,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe | ||||
| } | ||||
|  | ||||
| class GitBucketVerbatimSerializer extends VerbatimSerializer { | ||||
|   def serialize(node: VerbatimNode, printer: Printer) { | ||||
|   def serialize(node: VerbatimNode, printer: Printer): Unit = { | ||||
|     printer.println.print("<pre") | ||||
|     if (!StringUtils.isEmpty(node.getType)) { | ||||
|       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('"') | ||||
|   } | ||||
|  | ||||
|   override def visit(node: TextNode) { | ||||
|   override def visit(node: TextNode): Unit =  { | ||||
|     // convert commit id and username to link. | ||||
|     val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText | ||||
|  | ||||
|   | ||||
| @@ -9,12 +9,12 @@ import service.RequestCache | ||||
|  * Provides helper methods for Twirl templates. | ||||
|  */ | ||||
| object helpers extends AvatarImageProvider with LinkConverter with RequestCache { | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Format java.util.Date to "yyyy-MM-dd HH:mm:ss". | ||||
|    */ | ||||
|   def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Format java.util.Date to "yyyy-MM-dd". | ||||
|    */ | ||||
| @@ -31,9 +31,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|    * Converts Markdown of Wiki pages to HTML. | ||||
|    */ | ||||
|   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)) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns <img> 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 = | ||||
|     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 = | ||||
|     Html(message | ||||
|       .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"   , s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""") | ||||
|       .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""") | ||||
|       .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]"             , s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""") | ||||
|       .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""") | ||||
|       .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]"   , s"""<a href="${context.path}/$$1/$$2/tree/$$3">$$3</a>""") | ||||
|       .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""") | ||||
|       .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]"   , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""") | ||||
|       .replaceAll("\\[user:([^\\s]+?)\\]"                        , s"""<a href="${context.path}/$$1">$$1</a>""") | ||||
|     ) | ||||
|  | ||||
|   /** | ||||
|    * URL encode except '/'. | ||||
|    */ | ||||
|   def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/") | ||||
|  | ||||
|   def urlEncode(value: String): String = StringUtil.urlEncode(value) | ||||
|  | ||||
|   def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("") | ||||
| @@ -73,14 +93,12 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|   /** | ||||
|    * Generates the url to the account page. | ||||
|    */ | ||||
|   def url(userName: String)(implicit context: app.Context): String = | ||||
|     s"${context.path}/${userName}" | ||||
|   def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}" | ||||
|  | ||||
|   /** | ||||
|    * Returns the url to the root of assets. | ||||
|    */ | ||||
|   def assets(implicit context: app.Context): String = | ||||
|     s"${context.path}/assets" | ||||
|   def assets(implicit context: app.Context): String = s"${context.path}/assets" | ||||
|  | ||||
|   /** | ||||
|    * Generates the link to the account page. | ||||
| @@ -91,6 +109,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|     } getOrElse Html(userName) | ||||
|   } | ||||
|  | ||||
|   def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime | ||||
|  | ||||
|   /** | ||||
|    * Implicit conversion to add mkHtml() to Seq[Html]. | ||||
|    */ | ||||
|   | ||||
| @@ -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 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="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> | ||||
| @main(account, groupNames, "activity"){ | ||||
|   @helper.html.activities(activities) | ||||
| } | ||||
|   | ||||
| @@ -13,34 +13,43 @@ | ||||
|       <div class="span6"> | ||||
|         @if(account.isEmpty){ | ||||
|           <fieldset> | ||||
|             <label for="userName"><strong>User name</strong></label> | ||||
|             <label for="userName" class="strong">Username:</label> | ||||
|             <input type="text" name="userName" id="userName" value=""/> | ||||
|             <span id="error-userName" class="error"></span> | ||||
|           </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> | ||||
|           <label for="password"><strong>Password</strong> | ||||
|             @if(account.nonEmpty){ | ||||
|               (Input to change password) | ||||
|             } | ||||
|           </label> | ||||
|           <input type="password" name="password" id="password" value=""/> | ||||
|           <span id="error-password" class="error"></span> | ||||
|           <label for="fullName" class="strong">Full Name:</label> | ||||
|           <input type="text" name="fullName" id="fullName" value="@account.map(_.fullName)"/> | ||||
|           <span id="error-fullName" class="error"></span> | ||||
|         </fieldset> | ||||
|         <fieldset> | ||||
|           <label for="mailAddress"><strong>Mail Address</strong></label> | ||||
|           <label for="mailAddress" class="strong">Mail Address:</label> | ||||
|           <input type="text" name="mailAddress" id="mailAddress" value="@account.map(_.mailAddress)"/> | ||||
|           <span id="error-mailAddress" class="error"></span> | ||||
|         </fieldset> | ||||
|         <fieldset> | ||||
|           <label for="url"><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)"/> | ||||
|           <span id="error-url" class="error"></span> | ||||
|         </fieldset> | ||||
|       </div> | ||||
|       <div class="span6"> | ||||
|         <fieldset> | ||||
|           <label for="avatar"><strong>Image (Optional)</strong></label> | ||||
|           <label for="avatar" class="strong">Image (optional):</label> | ||||
|           @helper.html.uploadavatar(account) | ||||
|         </fieldset> | ||||
|       </div> | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/main/twirl/account/main.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/main/twirl/account/main.scala.html
									
									
									
									
									
										Normal 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> | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/main/twirl/account/members.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/main/twirl/account/members.scala.html
									
									
									
									
									
										Normal 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> | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -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 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="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, "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> | ||||
| @main(account, groupNames, "repositories"){ | ||||
|   @if(repositories.isEmpty){ | ||||
|     No repositories | ||||
|   } else { | ||||
|     @repositories.map { repository => | ||||
|       <div class="block"> | ||||
|         <div class="block-header"> | ||||
|           <a href="@url(repository)">@repository.name</a> | ||||
|           @if(repository.repository.isPrivate){ | ||||
|             <i class="icon-lock"></i> | ||||
|           } | ||||
|         </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> | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
| @@ -10,6 +10,9 @@ | ||||
|         <li@if(active=="system"){ class="active"}> | ||||
|           <a href="@path/admin/system">System Settings</a> | ||||
|         </li> | ||||
|         <li> | ||||
|           <a href="@path/console/login.jsp">H2 Console</a> | ||||
|         </li> | ||||
|      </ul> | ||||
|    </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -8,17 +8,151 @@ | ||||
|       <div class="box"> | ||||
|         <div class="box-header">System Settings</div> | ||||
|         <div class="box-content"> | ||||
|           <label><strong>Account registration</strong></label> | ||||
|           <!--====================================================================--> | ||||
|           <!-- Account registration --> | ||||
|           <!--====================================================================--> | ||||
|           <label class="strong">Account registration</label> | ||||
|           <fieldset> | ||||
|             <label> | ||||
|             <label class="radio"> | ||||
|               <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 class="radio"> | ||||
|               <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> | ||||
|           </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> | ||||
|       <fieldset> | ||||
| @@ -26,4 +160,15 @@ | ||||
|       </fieldset> | ||||
|     </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> | ||||
| @@ -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> | ||||
|   } | ||||
| } | ||||
							
								
								
									
										116
									
								
								src/main/twirl/admin/users/group.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/main/twirl/admin/users/group.scala.html
									
									
									
									
									
										Normal 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> | ||||
| @@ -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 view.helpers._ | ||||
| @html.main("Manage Users"){ | ||||
|   @admin.html.menu("users"){ | ||||
|     <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> | ||||
|     <table class="table table-bordered table-hover"> | ||||
|       @users.map { account => | ||||
|         <tr> | ||||
|           <td> | ||||
|             <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 class="strong"> | ||||
|               @avatar(account.userName, 20) | ||||
|               <a href="@url(account.userName)">@account.userName</a> | ||||
|               @if(account.isAdmin){ | ||||
|                 (Administrator) | ||||
|               @if(account.isGroupAccount){ | ||||
|                 (Group) | ||||
|               } else { | ||||
|                 (Normal) | ||||
|                 @if(account.isAdmin){ | ||||
|                   (Administrator) | ||||
|                 } else { | ||||
|                   (Normal) | ||||
|                 } | ||||
|               } | ||||
|               @if(account.isGroupAccount){ | ||||
|                 @members(account.userName).map { userName => | ||||
|                   @avatar(userName, 20, tooltip = true) | ||||
|                 } | ||||
|               } | ||||
|             </div> | ||||
|             <div> | ||||
|               <hr> | ||||
|               <i class="icon-envelope"></i> @account.mailAddress | ||||
|               @if(!account.isGroupAccount){ | ||||
|                 <i class="icon-envelope"></i> @account.mailAddress | ||||
|               } | ||||
|               @account.url.map { url => | ||||
|                 <i class="icon-home"></i> @url | ||||
|               } | ||||
| @@ -32,7 +48,9 @@ | ||||
|             <div> | ||||
|               <span class="muted">Registered:</span> @datetime(account.registeredDate) | ||||
|               <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> | ||||
|           </td> | ||||
|         </tr> | ||||
|   | ||||
							
								
								
									
										74
									
								
								src/main/twirl/admin/users/user.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/main/twirl/admin/users/user.scala.html
									
									
									
									
									
										Normal 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> | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/main/twirl/dashboard/issues.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/main/twirl/dashboard/issues.scala.html
									
									
									
									
									
										Normal 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> | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/main/twirl/dashboard/pulls.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/main/twirl/dashboard/pulls.scala.html
									
									
									
									
									
										Normal 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> | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/main/twirl/dashboard/tab.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/main/twirl/dashboard/tab.scala.html
									
									
									
									
									
										Normal 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> | ||||
| @@ -1,10 +1,30 @@ | ||||
| @(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) | ||||
| @import context._ | ||||
| @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"> | ||||
|   <a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)">@repository.name</a> | ||||
|   @if(repository.repository.isPrivate){ | ||||
|     <i class="icon-lock"></i> | ||||
|       <i class="icon-lock"></i> | ||||
|   } | ||||
|   @if(!repository.repository.isPrivate){ | ||||
|       <i class="icon-eye-open"></i> | ||||
|   } | ||||
|   <a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a> | ||||
|    | ||||
|   @defining(repository.repository){ x => | ||||
|     @if(repository.repository.originRepositoryName.isDefined){ | ||||
|       <div class="forked"> | ||||
|         forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a> | ||||
|       </div> | ||||
|     } | ||||
|   } | ||||
| </div> | ||||
| <table class="global-nav box-header"> | ||||
| @@ -14,16 +34,28 @@ | ||||
|     </th> | ||||
|     <th class="box-header@if(active=="issues"){ active}"> | ||||
|       <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 class="box-header@if(active=="wiki"){ active}"> | ||||
|       <a href="@url(repository)/wiki">Wiki</a> | ||||
|     </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)){ | ||||
|     <th class="box-header@if(active=="settings"){ active}"> | ||||
|       <a href="@url(repository)/settings">Settings</a> | ||||
|     </th> | ||||
|     } | ||||
|   </tr>   | ||||
|   </tr> | ||||
| </table> | ||||
| <script type="text/javascript"> | ||||
| $(function(){ | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/main/twirl/helper/account.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/main/twirl/helper/account.scala.html
									
									
									
									
									
										Normal 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> | ||||
| @@ -1,43 +1,96 @@ | ||||
| @(activities: List[model.Activity])(implicit context: app.Context) | ||||
| @import context._ | ||||
| @import view.helpers._ | ||||
|  | ||||
| @if(activities.isEmpty){ | ||||
|   No activity | ||||
| } else { | ||||
|   @activities.map { activity => | ||||
|     <div class="block"> | ||||
|       <div class="muted small">@datetime(activity.activityDate)</div> | ||||
|       <div class="strong"> | ||||
|         @avatar(activity.activityUserName, 16) | ||||
|         @activityMessage(activity.message) | ||||
|       </div> | ||||
|       @activity.additionalInfo.map { additionalInfo => | ||||
|         @(activity.activityType match { | ||||
|           case "create_wiki" => { | ||||
|             <div class="small activity-message">Created <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div> | ||||
|           } | ||||
|           case "edit_wiki" => { | ||||
|             <div class="small activity-message">Edited <a href={s"${path}/${activity.userName}/${activity.repositoryName}/wiki/${additionalInfo}"}>{additionalInfo}</a>.</div> | ||||
|           } | ||||
|           case "push" => { | ||||
|             <div class="small activity-message"> | ||||
|               {additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => | ||||
|                 if(i == 3){ | ||||
|                   <div>...</div> | ||||
|                 } else { | ||||
|       @(activity.activityType match { | ||||
|         case "open_issue"        => detailActivity(activity, "activity-issue.png") | ||||
|         case "comment_issue"     => detailActivity(activity, "activity-comment.png") | ||||
|         case "close_issue"       => detailActivity(activity, "activity-issue-close.png") | ||||
|         case "reopen_issue"      => detailActivity(activity, "activity-issue-reopen.png") | ||||
|         case "open_pullreq"      => detailActivity(activity, "activity-merge.png") | ||||
|         case "merge_pullreq"     => detailActivity(activity, "activity-merge.png") | ||||
|         case "create_repository" => simpleActivity(activity, "activity-create-repository.png") | ||||
|         case "create_branch"     => simpleActivity(activity, "activity-branch.png") | ||||
|         case "create_tag"        => simpleActivity(activity, "activity-tag.png") | ||||
|         case "fork"              => simpleActivity(activity, "activity-fork.png") | ||||
|         case "push"  => customActivity(activity, "activity-commit.png"){ | ||||
|           <div class="small activity-message"> | ||||
|             {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => | ||||
|               if(i == 3){ | ||||
|                 <div>...</div> | ||||
|               } else { | ||||
|                 if(commit.nonEmpty){ | ||||
|                   <div> | ||||
|                     <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> | ||||
|                      <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> | ||||
|                   </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> | ||||
|   } | ||||
| } | ||||
|  | ||||
| @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
		Reference in New Issue
	
	Block a user