mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-11-03 20:15:59 +01:00 
			
		
		
		
	Compare commits
	
		
			632 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					5674f0e980 | ||
| 
						 | 
					b9ade60eb2 | ||
| 
						 | 
					96303723fa | ||
| 
						 | 
					0f5dbc5788 | ||
| 
						 | 
					8df0c3a439 | ||
| 
						 | 
					ca6a86816a | ||
| 
						 | 
					3ea939798f | ||
| 
						 | 
					d947410e3c | ||
| 
						 | 
					db59bc08ac | ||
| 
						 | 
					95a8649f79 | ||
| 
						 | 
					ffd10122ed | ||
| 
						 | 
					c4c39f36e9 | ||
| 
						 | 
					96900c3cbf | ||
| 
						 | 
					69fa370d12 | ||
| 
						 | 
					7496437d11 | ||
| 
						 | 
					33b7d09af7 | ||
| 
						 | 
					53d0974760 | ||
| 
						 | 
					a87399f223 | ||
| 
						 | 
					975dfb17e1 | ||
| 
						 | 
					8b8bd0289b | ||
| 
						 | 
					3bb69c623b | ||
| 
						 | 
					dd427bdbef | ||
| 
						 | 
					b40657a14a | ||
| 
						 | 
					21ca5b2eec | ||
| 
						 | 
					b78d584d8a | ||
| 
						 | 
					e6b666a66a | ||
| 
						 | 
					bab93ea4f5 | ||
| 
						 | 
					7fe98253ae | ||
| 
						 | 
					13385cbced | ||
| 
						 | 
					3f20cec7b2 | ||
| 
						 | 
					a0e4b020ca | ||
| 
						 | 
					ea5d898b27 | ||
| 
						 | 
					4e652b5ccd | ||
| 
						 | 
					dd809896c8 | ||
| 
						 | 
					93536d3365 | ||
| 
						 | 
					098b18fe6d | ||
| 
						 | 
					66efdac757 | ||
| 
						 | 
					45545d3815 | ||
| 
						 | 
					b65d41731b | ||
| 
						 | 
					be19e97518 | ||
| 
						 | 
					2ebf2b99bd | ||
| 
						 | 
					be79ac2eb2 | ||
| 
						 | 
					05afec3236 | ||
| 
						 | 
					57879eb72e | ||
| 
						 | 
					2bc915f51b | ||
| 
						 | 
					1ca55805b5 | ||
| 
						 | 
					93cc1be166 | ||
| 
						 | 
					f88ce3f671 | ||
| 
						 | 
					20aabfc273 | ||
| 
						 | 
					601f8c4249 | ||
| 
						 | 
					d0ccfc52b8 | ||
| 
						 | 
					c22aef8ee2 | ||
| 
						 | 
					3807e61a48 | ||
| 
						 | 
					55722f87af | ||
| 
						 | 
					212f3725ed | ||
| 
						 | 
					193a312b22 | ||
| 
						 | 
					6a2d2ebfd1 | ||
| 
						 | 
					82beed1f44 | ||
| 
						 | 
					0ede7e9921 | ||
| 
						 | 
					6d200aa340 | ||
| 
						 | 
					a0fbb90048 | ||
| 
						 | 
					08e29e7077 | ||
| 
						 | 
					d2317d0a97 | ||
| 
						 | 
					972628eb65 | ||
| 
						 | 
					51a56356cb | ||
| 
						 | 
					3bef71f5f2 | ||
| 
						 | 
					2bb1f6168a | ||
| 
						 | 
					b13820fc0e | ||
| 
						 | 
					723de9e81e | ||
| 
						 | 
					3e161353ed | ||
| 
						 | 
					2a8706630a | ||
| 
						 | 
					121b6ee641 | ||
| 
						 | 
					34e299bf52 | ||
| 
						 | 
					0822b7b5f3 | ||
| 
						 | 
					618110327a | ||
| 
						 | 
					f58f476060 | ||
| 
						 | 
					f5a544603a | ||
| 
						 | 
					89515cd087 | ||
| 
						 | 
					37731c4163 | ||
| 
						 | 
					1d4720d784 | ||
| 
						 | 
					a10b053489 | ||
| 
						 | 
					6122c8a1e1 | ||
| 
						 | 
					fa9254c240 | ||
| 
						 | 
					10616bca7d | ||
| 
						 | 
					307f7e15e9 | ||
| 
						 | 
					86cf97d76b | ||
| 
						 | 
					01f6590c04 | ||
| 
						 | 
					8f0c22bae9 | ||
| 
						 | 
					652a68c5b1 | ||
| 
						 | 
					1f56e1360d | ||
| 
						 | 
					38475ffefe | ||
| 
						 | 
					7a44a4d726 | ||
| 
						 | 
					9dbc0c3fd6 | ||
| 
						 | 
					56bb43ea6b | ||
| 
						 | 
					b287c1f60d | ||
| 
						 | 
					258d53b7a6 | ||
| 
						 | 
					2e11d6dd78 | ||
| 
						 | 
					a2a2e22485 | ||
| 
						 | 
					c182cde14b | ||
| 
						 | 
					104c3bc89d | ||
| 
						 | 
					2668977918 | ||
| 
						 | 
					28424c96c4 | ||
| 
						 | 
					9cfa8c594b | ||
| 
						 | 
					5c70cd654c | ||
| 
						 | 
					7aca24e51d | ||
| 
						 | 
					cce0b67871 | ||
| 
						 | 
					606cd83f44 | ||
| 
						 | 
					32897c36f9 | ||
| 
						 | 
					92e4e12655 | ||
| 
						 | 
					c8e5b75165 | ||
| 
						 | 
					09b9a52ad3 | ||
| 
						 | 
					33378c6464 | ||
| 
						 | 
					259bcfc14f | ||
| 
						 | 
					c361d24ba4 | ||
| 
						 | 
					d5e1b18b52 | ||
| 
						 | 
					684a17a15b | ||
| 
						 | 
					66b7b69d20 | ||
| 
						 | 
					57254f6366 | ||
| 
						 | 
					c64909ab1a | ||
| 
						 | 
					34dd8541f4 | ||
| 
						 | 
					50b4fb154d | ||
| 
						 | 
					0b3781ec8a | ||
| 
						 | 
					0e1d184715 | ||
| 
						 | 
					d8c27046f6 | ||
| 
						 | 
					fd09058a7d | ||
| 
						 | 
					1c99b57709 | ||
| 
						 | 
					9ee739d102 | ||
| 
						 | 
					e2cde81b72 | ||
| 
						 | 
					84a4b8fd92 | ||
| 
						 | 
					d2c94909cb | ||
| 
						 | 
					3683a5fb7d | ||
| 
						 | 
					1223bf2fd8 | ||
| 
						 | 
					a9bfe0dfab | ||
| 
						 | 
					9af81c7093 | ||
| 
						 | 
					1e8a5c3cde | ||
| 
						 | 
					707ad866e1 | ||
| 
						 | 
					c3a944b40e | ||
| 
						 | 
					ab80cb8f60 | ||
| 
						 | 
					4f45e047d2 | ||
| 
						 | 
					bbe455ac49 | ||
| 
						 | 
					b5f173fa46 | ||
| 
						 | 
					4bd6ef143a | ||
| 
						 | 
					fd4a696303 | ||
| 
						 | 
					4af4c4e7c6 | ||
| 
						 | 
					3b2e42fd61 | ||
| 
						 | 
					b07d0b028f | ||
| 
						 | 
					f3900ca8f9 | ||
| 
						 | 
					62d43f120a | ||
| 
						 | 
					c4f69fbd13 | ||
| 
						 | 
					fece20ff40 | ||
| 
						 | 
					bbef4b22ca | ||
| 
						 | 
					481a2d213f | ||
| 
						 | 
					8ed4075f1e | ||
| 
						 | 
					9bf82733d1 | ||
| 
						 | 
					30d66f95bc | ||
| 
						 | 
					378c2c39a8 | ||
| 
						 | 
					daf5fc434c | ||
| 
						 | 
					e5bf90ed26 | ||
| 
						 | 
					1bf3146220 | ||
| 
						 | 
					ddd51850f0 | ||
| 
						 | 
					e14a0c3770 | ||
| 
						 | 
					b0b318ce30 | ||
| 
						 | 
					6f666ca49f | ||
| 
						 | 
					0cb2116bdf | ||
| 
						 | 
					280113497b | ||
| 
						 | 
					5f6e318329 | ||
| 
						 | 
					f8921b6f10 | ||
| 
						 | 
					31a08abff2 | ||
| 
						 | 
					0fa1e11c5a | ||
| 
						 | 
					e2c99a46be | ||
| 
						 | 
					1edff41690 | ||
| 
						 | 
					6d6f529d40 | ||
| 
						 | 
					e2fd7d9d8e | ||
| 
						 | 
					61146687b3 | ||
| 
						 | 
					1d1f7fa581 | ||
| 
						 | 
					67da88fab5 | ||
| 
						 | 
					fb3ed70215 | ||
| 
						 | 
					2fceeeee4e | ||
| 
						 | 
					67102822e8 | ||
| 
						 | 
					d00a0f1571 | ||
| 
						 | 
					db5395ddbc | ||
| 
						 | 
					7698f12112 | ||
| 
						 | 
					1e8224536b | ||
| 
						 | 
					a846c77c7e | ||
| 
						 | 
					29812f4a82 | ||
| 
						 | 
					a863951d97 | ||
| 
						 | 
					146be677ba | ||
| 
						 | 
					03b5f7feb8 | ||
| 
						 | 
					6d54361a6d | ||
| 
						 | 
					f440421ed1 | ||
| 
						 | 
					e57464fc5e | ||
| 
						 | 
					2a4b0f5ddb | ||
| 
						 | 
					bb66e2201f | ||
| 
						 | 
					4dc60e887f | ||
| 
						 | 
					f6eb2e2dc8 | ||
| 
						 | 
					9ecc10ab21 | ||
| 
						 | 
					d7037a43c6 | ||
| 
						 | 
					2471b8dfe0 | ||
| 
						 | 
					0430cb49f9 | ||
| 
						 | 
					7811926779 | ||
| 
						 | 
					9bb66a4297 | ||
| 
						 | 
					70772f0d74 | ||
| 
						 | 
					728b00e4c3 | ||
| 
						 | 
					97008ef984 | ||
| 
						 | 
					6b86406e94 | ||
| 
						 | 
					4252c364a4 | ||
| 
						 | 
					4f4bc0321b | ||
| 
						 | 
					6ecabe4588 | ||
| 
						 | 
					93fa8484c5 | ||
| 
						 | 
					ff2e55e82c | ||
| 
						 | 
					259637ce3c | ||
| 
						 | 
					743b9b759a | ||
| 
						 | 
					73ba0b348b | ||
| 
						 | 
					e93769cc81 | ||
| 
						 | 
					68f9739eed | ||
| 
						 | 
					c3d25b7a71 | ||
| 
						 | 
					aaa582ff1a | ||
| 
						 | 
					debc798aec | ||
| 
						 | 
					6042f0e1e0 | ||
| 
						 | 
					e10d02f45c | ||
| 
						 | 
					aebf4ff728 | ||
| 
						 | 
					1a2e89c9ed | ||
| 
						 | 
					e10e2748b9 | ||
| 
						 | 
					f422936e34 | ||
| 
						 | 
					4e87f21405 | ||
| 
						 | 
					dc2d79b16c | ||
| 
						 | 
					88a3100563 | ||
| 
						 | 
					8d3433a0e7 | ||
| 
						 | 
					0fe30e5629 | ||
| 
						 | 
					ea1e9037c4 | ||
| 
						 | 
					24feeb17be | ||
| 
						 | 
					6a7fc55572 | ||
| 
						 | 
					cf047a8cee | ||
| 
						 | 
					896420f8dc | ||
| 
						 | 
					619f72d929 | ||
| 
						 | 
					dc21e8388e | ||
| 
						 | 
					8c35310cd6 | ||
| 
						 | 
					642e8bbb7c | ||
| 
						 | 
					3ee4143235 | ||
| 
						 | 
					c136823170 | ||
| 
						 | 
					92631fbfcf | ||
| 
						 | 
					5a1b1a4485 | ||
| 
						 | 
					3e82534c78 | ||
| 
						 | 
					dd694d27b5 | ||
| 
						 | 
					1900aefe32 | ||
| 
						 | 
					2fe6b8c1e7 | ||
| 
						 | 
					ecfaa0247a | ||
| 
						 | 
					9a0cc9e043 | ||
| 
						 | 
					b0360db105 | ||
| 
						 | 
					0f9c95c15a | ||
| 
						 | 
					8efd1da7e6 | ||
| 
						 | 
					52ebba43d5 | ||
| 
						 | 
					790eee7443 | ||
| 
						 | 
					9f325290e8 | ||
| 
						 | 
					93bf0a9a47 | ||
| 
						 | 
					bdd0af21a9 | ||
| 
						 | 
					aae5fe387b | ||
| 
						 | 
					257c5aef51 | ||
| 
						 | 
					3cae337487 | ||
| 
						 | 
					779df30ec8 | ||
| 
						 | 
					5609507991 | ||
| 
						 | 
					1c24090c14 | ||
| 
						 | 
					7da2c650d2 | ||
| 
						 | 
					27fa9df2ee | ||
| 
						 | 
					63c4e12259 | ||
| 
						 | 
					1f66670819 | ||
| 
						 | 
					a7b4f8de8d | ||
| 
						 | 
					ad0d57fbf9 | ||
| 
						 | 
					cfc594805b | ||
| 
						 | 
					52461e673c | ||
| 
						 | 
					a97edb7ef5 | ||
| 
						 | 
					7a1c872861 | ||
| 
						 | 
					0e5591017a | ||
| 
						 | 
					a104157c9a | ||
| 
						 | 
					ad244adbfa | ||
| 
						 | 
					3721b328a6 | ||
| 
						 | 
					dd688f48b7 | ||
| 
						 | 
					296a0b2124 | ||
| 
						 | 
					b9cc46e5ef | ||
| 
						 | 
					375211fc30 | ||
| 
						 | 
					b8b59f9dcd | ||
| 
						 | 
					6760ff34ef | ||
| 
						 | 
					c5de7811c4 | ||
| 
						 | 
					82ef5457b0 | ||
| 
						 | 
					d558476cd2 | ||
| 
						 | 
					644701d995 | ||
| 
						 | 
					1382d59206 | ||
| 
						 | 
					b60e2c07c7 | ||
| 
						 | 
					86f0307633 | ||
| 
						 | 
					1db891a771 | ||
| 
						 | 
					c9fa3291f5 | ||
| 
						 | 
					e0f1658120 | ||
| 
						 | 
					da105b7180 | ||
| 
						 | 
					9c4f7cc530 | ||
| 
						 | 
					d7eef8bd25 | ||
| 
						 | 
					7b7c0e1eee | ||
| 
						 | 
					2ae7798591 | ||
| 
						 | 
					3f76453f34 | ||
| 
						 | 
					8fbbe7f31e | ||
| 
						 | 
					92a43b4f99 | ||
| 
						 | 
					c128086778 | ||
| 
						 | 
					cc4fb8bf79 | ||
| 
						 | 
					c3ac0f3d9f | ||
| 
						 | 
					dfa4816633 | ||
| 
						 | 
					06978a4fc4 | ||
| 
						 | 
					3a2ecf6896 | ||
| 
						 | 
					b357d52ec5 | ||
| 
						 | 
					f8b6b1ebf8 | ||
| 
						 | 
					91bd9d1111 | ||
| 
						 | 
					1ec825050d | ||
| 
						 | 
					a6a08d13e9 | ||
| 
						 | 
					9a47c4a990 | ||
| 
						 | 
					5063294177 | ||
| 
						 | 
					b14917e2c6 | ||
| 
						 | 
					c1bbec2a1c | ||
| 
						 | 
					6227a4643a | ||
| 
						 | 
					00af52815d | ||
| 
						 | 
					5d3365a944 | ||
| 
						 | 
					84ac2974fb | ||
| 
						 | 
					c9a1515d1f | ||
| 
						 | 
					5317ac5e03 | ||
| 
						 | 
					9df1467ddf | ||
| 
						 | 
					df79bd4515 | ||
| 
						 | 
					cbb14f2ba8 | ||
| 
						 | 
					1fe649e70f | ||
| 
						 | 
					0d918add28 | ||
| 
						 | 
					3926c98338 | ||
| 
						 | 
					3bff6a1949 | ||
| 
						 | 
					ec0c964ceb | ||
| 
						 | 
					b4fd90c6d3 | ||
| 
						 | 
					7dfd63cfa2 | ||
| 
						 | 
					a562e5ca14 | ||
| 
						 | 
					2885eef4ab | ||
| 
						 | 
					087297d14c | ||
| 
						 | 
					6e0fb95ac3 | ||
| 
						 | 
					61e28146fb | ||
| 
						 | 
					40d3f0ef9e | ||
| 
						 | 
					99db825114 | ||
| 
						 | 
					7341b377fe | ||
| 
						 | 
					7f78a98de0 | ||
| 
						 | 
					a64207f0ec | ||
| 
						 | 
					d86f40e3a2 | ||
| 
						 | 
					b74417f393 | ||
| 
						 | 
					f5883abf04 | ||
| 
						 | 
					02a367fd99 | ||
| 
						 | 
					4870533710 | ||
| 
						 | 
					9175cf5c71 | ||
| 
						 | 
					8170a1b01d | ||
| 
						 | 
					d1c6c763e2 | ||
| 
						 | 
					0c683f7243 | ||
| 
						 | 
					63de780527 | ||
| 
						 | 
					c5ccbf2d1f | ||
| 
						 | 
					8777535431 | ||
| 
						 | 
					70192ce420 | ||
| 
						 | 
					a74bbd3eeb | ||
| 
						 | 
					02d79cb16a | ||
| 
						 | 
					78ca9b3f1a | ||
| 
						 | 
					017631e337 | ||
| 
						 | 
					f9078dff2c | ||
| 
						 | 
					b66381d677 | ||
| 
						 | 
					49bf88f7a7 | ||
| 
						 | 
					f93ceaa91d | ||
| 
						 | 
					0fe122dc63 | ||
| 
						 | 
					4e2a3fdbd0 | ||
| 
						 | 
					3d251fa8ad | ||
| 
						 | 
					af0b52448a | ||
| 
						 | 
					8d200c72d3 | ||
| 
						 | 
					f78cdb637d | ||
| 
						 | 
					845f2d6faa | ||
| 
						 | 
					525edbab80 | ||
| 
						 | 
					c422b1c9a5 | ||
| 
						 | 
					1043b13228 | ||
| 
						 | 
					5e6c33df6c | ||
| 
						 | 
					9541771703 | ||
| 
						 | 
					f99d37cfad | ||
| 
						 | 
					0cfe31ccd9 | ||
| 
						 | 
					8fc1a5473b | ||
| 
						 | 
					049b12b908 | ||
| 
						 | 
					45f992b2bc | ||
| 
						 | 
					9e2c66c341 | ||
| 
						 | 
					2d0f59b6f2 | ||
| 
						 | 
					fbba29e810 | ||
| 
						 | 
					07a108760c | ||
| 
						 | 
					b641bfb56a | ||
| 
						 | 
					c65d80bc72 | ||
| 
						 | 
					e0d266bf16 | ||
| 
						 | 
					b62f7c5aee | ||
| 
						 | 
					c89f04b926 | ||
| 
						 | 
					ff8b4b4a88 | ||
| 
						 | 
					07d63ae63a | ||
| 
						 | 
					c0f5cb1641 | ||
| 
						 | 
					50d84835cb | ||
| 
						 | 
					8cdf4ef618 | ||
| 
						 | 
					eff3a7acb4 | ||
| 
						 | 
					18cd967a9c | ||
| 
						 | 
					328d6c1d17 | ||
| 
						 | 
					716eddac7b | ||
| 
						 | 
					9b15af3bb7 | ||
| 
						 | 
					b732e0d55a | ||
| 
						 | 
					d92a1cee1c | ||
| 
						 | 
					10a40bfcaf | ||
| 
						 | 
					af397ba150 | ||
| 
						 | 
					c7a2ec8290 | ||
| 
						 | 
					145c155ba5 | ||
| 
						 | 
					6f9ef32d96 | ||
| 
						 | 
					aa5b9dbbbd | ||
| 
						 | 
					f11be44c02 | ||
| 
						 | 
					4276c8f23e | ||
| 
						 | 
					9e1352c8b1 | ||
| 
						 | 
					d46589ad29 | ||
| 
						 | 
					09b7e67c52 | ||
| 
						 | 
					79e1abe624 | ||
| 
						 | 
					3db3bf1b74 | ||
| 
						 | 
					a335c31385 | ||
| 
						 | 
					9bd1f0a492 | ||
| 
						 | 
					7a2c82461e | ||
| 
						 | 
					21f7888f55 | ||
| 
						 | 
					97349a9bb2 | ||
| 
						 | 
					ce3b6ed7c2 | ||
| 
						 | 
					e3fd564efd | ||
| 
						 | 
					5cf96134d5 | ||
| 
						 | 
					607c477e7d | ||
| 
						 | 
					5e0619b500 | ||
| 
						 | 
					17920e1195 | ||
| 
						 | 
					721454aa90 | ||
| 
						 | 
					d870896cfb | ||
| 
						 | 
					270eb7cf1d | ||
| 
						 | 
					527fd94145 | ||
| 
						 | 
					04e4572088 | ||
| 
						 | 
					0961eb5976 | ||
| 
						 | 
					0311359922 | ||
| 
						 | 
					ec09adf03e | ||
| 
						 | 
					b031103df8 | ||
| 
						 | 
					7701521a2e | ||
| 
						 | 
					0c683d4f75 | ||
| 
						 | 
					200d095034 | ||
| 
						 | 
					94576a876a | ||
| 
						 | 
					0fa1922bb0 | ||
| 
						 | 
					c557905858 | ||
| 
						 | 
					31b21d74b1 | ||
| 
						 | 
					153244c390 | ||
| 
						 | 
					e97b5c3c89 | ||
| 
						 | 
					374893a5ae | ||
| 
						 | 
					17f581f654 | ||
| 
						 | 
					590b431ec1 | ||
| 
						 | 
					98266fe0e1 | ||
| 
						 | 
					2e236e90ba | ||
| 
						 | 
					c5aee0810c | ||
| 
						 | 
					f13d757976 | ||
| 
						 | 
					7a0a62af2d | ||
| 
						 | 
					ceab1d2fd2 | ||
| 
						 | 
					639e7e0b3f | ||
| 
						 | 
					89601305f6 | ||
| 
						 | 
					4600b5a3bf | ||
| 
						 | 
					b620307983 | ||
| 
						 | 
					891ca70ade | ||
| 
						 | 
					9ed2a50d26 | ||
| 
						 | 
					cbf615d699 | ||
| 
						 | 
					97b1a0090d | ||
| 
						 | 
					9078aa6d08 | ||
| 
						 | 
					8677146a8d | ||
| 
						 | 
					2c14dfb781 | ||
| 
						 | 
					057c5f073c | ||
| 
						 | 
					e902da6595 | ||
| 
						 | 
					8b5414c8f7 | ||
| 
						 | 
					c86ece4dc0 | ||
| 
						 | 
					1f71619b6b | ||
| 
						 | 
					5b34b9c795 | ||
| 
						 | 
					99d15899f6 | ||
| 
						 | 
					c114a8b507 | ||
| 
						 | 
					0dd37c2481 | ||
| 
						 | 
					b5d7c96bba | ||
| 
						 | 
					a76792ced4 | ||
| 
						 | 
					39091240ff | ||
| 
						 | 
					0ccb753892 | ||
| 
						 | 
					63dda84c8b | ||
| 
						 | 
					7ba1f85d48 | ||
| 
						 | 
					bb9a23fe0f | ||
| 
						 | 
					8536824d7e | ||
| 
						 | 
					78073babe4 | ||
| 
						 | 
					521d15219c | ||
| 
						 | 
					7469a3c349 | ||
| 
						 | 
					153a32e340 | ||
| 
						 | 
					f155d4f150 | ||
| 
						 | 
					d683dd2c38 | ||
| 
						 | 
					7ebba741a8 | ||
| 
						 | 
					d10f683098 | ||
| 
						 | 
					0270133ecf | ||
| 
						 | 
					d7b479d97d | ||
| 
						 | 
					4366c512fe | ||
| 
						 | 
					229a773ed2 | ||
| 
						 | 
					d882f20436 | ||
| 
						 | 
					9d7235af20 | ||
| 
						 | 
					c2eb53d154 | ||
| 
						 | 
					7629e347df | ||
| 
						 | 
					2764caae29 | ||
| 
						 | 
					a87bd2a928 | ||
| 
						 | 
					202c920064 | ||
| 
						 | 
					a08316bba0 | ||
| 
						 | 
					520e5ebb7a | ||
| 
						 | 
					5d5a4cacb1 | ||
| 
						 | 
					b885a1a0d4 | ||
| 
						 | 
					1705bd3ae9 | ||
| 
						 | 
					e87c69f989 | ||
| 
						 | 
					1c529eea3d | ||
| 
						 | 
					738b0cfe9a | ||
| 
						 | 
					913561cb2a | ||
| 
						 | 
					05a91565dc | ||
| 
						 | 
					79827efe9b | ||
| 
						 | 
					8722cd89fc | ||
| 
						 | 
					52fcc4ad1e | ||
| 
						 | 
					59a096bfd6 | ||
| 
						 | 
					5a1f541e13 | ||
| 
						 | 
					94bd1c6a93 | ||
| 
						 | 
					5b1aef5e52 | ||
| 
						 | 
					89bfcdc44e | ||
| 
						 | 
					fba81138ea | ||
| 
						 | 
					d50e07265e | ||
| 
						 | 
					c92891538e | ||
| 
						 | 
					ccc1e9bc8b | ||
| 
						 | 
					f33b398428 | ||
| 
						 | 
					226a8af262 | ||
| 
						 | 
					ebcc5ab4b1 | ||
| 
						 | 
					10e16e8379 | ||
| 
						 | 
					df1f3d8a00 | ||
| 
						 | 
					5e2dfffe25 | ||
| 
						 | 
					897f2ea6dd | ||
| 
						 | 
					3ff39ec578 | ||
| 
						 | 
					3d852a535d | ||
| 
						 | 
					6f6a61f31a | ||
| 
						 | 
					10f54f5790 | ||
| 
						 | 
					0e7280585a | ||
| 
						 | 
					1da7173f27 | ||
| 
						 | 
					1cb1e68a01 | ||
| 
						 | 
					b59c8a5512 | ||
| 
						 | 
					fe63ad0976 | ||
| 
						 | 
					941cb7b851 | ||
| 
						 | 
					d1cf0d9fd7 | ||
| 
						 | 
					64c2bb4d6b | ||
| 
						 | 
					24c9f5c17e | ||
| 
						 | 
					d368e4e80d | ||
| 
						 | 
					5c0ff84fc4 | ||
| 
						 | 
					502a21b6b6 | ||
| 
						 | 
					0e9bf59c0f | ||
| 
						 | 
					108f9fccdd | ||
| 
						 | 
					ac884bd7c3 | ||
| 
						 | 
					a4cb5c991c | ||
| 
						 | 
					68f1f55f37 | ||
| 
						 | 
					1dc779d5e8 | ||
| 
						 | 
					f781c7a08c | ||
| 
						 | 
					a8511a9f39 | ||
| 
						 | 
					47714eec45 | ||
| 
						 | 
					c46e9b2f4d | ||
| 
						 | 
					26d579f13f | ||
| 
						 | 
					6556d26742 | ||
| 
						 | 
					608dce2205 | ||
| 
						 | 
					f86e50c723 | ||
| 
						 | 
					b60fe33886 | ||
| 
						 | 
					5210a143fd | ||
| 
						 | 
					dc78dc9b0d | ||
| 
						 | 
					6b11c1a180 | ||
| 
						 | 
					b3669f6d66 | ||
| 
						 | 
					bbff75e037 | ||
| 
						 | 
					7e10618ceb | ||
| 
						 | 
					7f4def6b83 | ||
| 
						 | 
					5790d246c8 | ||
| 
						 | 
					19dee09c86 | ||
| 
						 | 
					dfe2889912 | ||
| 
						 | 
					223ba791fe | ||
| 
						 | 
					0d49bbe7ac | ||
| 
						 | 
					8381e8122a | ||
| 
						 | 
					f38924c7fe | ||
| 
						 | 
					43152c9341 | ||
| 
						 | 
					cf84e8b7cc | ||
| 
						 | 
					2b42e73530 | ||
| 
						 | 
					60030959f2 | ||
| 
						 | 
					7174523ac5 | ||
| 
						 | 
					f573fef9eb | ||
| 
						 | 
					b4250d8254 | ||
| 
						 | 
					ac4d4de3c1 | ||
| 
						 | 
					05e6d008fa | ||
| 
						 | 
					dd4abb2073 | ||
| 
						 | 
					612aba1365 | ||
| 
						 | 
					94dce09570 | ||
| 
						 | 
					cc241c5a7b | ||
| 
						 | 
					13cf9d01f0 | ||
| 
						 | 
					47453fec3f | ||
| 
						 | 
					641d506559 | ||
| 
						 | 
					3dec2b8159 | ||
| 
						 | 
					a0bd969140 | ||
| 
						 | 
					b30d42a37b | ||
| 
						 | 
					a03acc68e7 | ||
| 
						 | 
					05296473d3 | ||
| 
						 | 
					2118f8c764 | ||
| 
						 | 
					e366af98b5 | ||
| 
						 | 
					81e2ac44c3 | ||
| 
						 | 
					07bb326c06 | ||
| 
						 | 
					bcc2c8cc2d | ||
| 
						 | 
					2e0e17f1aa | ||
| 
						 | 
					c517b44e82 | ||
| 
						 | 
					f311339786 | ||
| 
						 | 
					34853d0322 | ||
| 
						 | 
					9c60b69c88 | ||
| 
						 | 
					4f10bccf84 | ||
| 
						 | 
					c7eaebf597 | ||
| 
						 | 
					60e1052d33 | ||
| 
						 | 
					7e77c102b0 | ||
| 
						 | 
					a452c582ab | ||
| 
						 | 
					0d3adb074d | ||
| 
						 | 
					8ec4b52dda | ||
| 
						 | 
					9265c68383 | ||
| 
						 | 
					4bd2d78ecb | ||
| 
						 | 
					e7aa766d0a | ||
| 
						 | 
					7d8300b3ce | ||
| 
						 | 
					af8a1234ed | ||
| 
						 | 
					bd0ecd0a9d | ||
| 
						 | 
					35c8f02f90 | ||
| 
						 | 
					f160952817 | ||
| 
						 | 
					9e5a302ab1 | ||
| 
						 | 
					a1dc19fa26 | ||
| 
						 | 
					e79ded934f | ||
| 
						 | 
					ef3e7d9286 | ||
| 
						 | 
					68b25ddbb5 | ||
| 
						 | 
					f96040eade | ||
| 
						 | 
					599a808054 | ||
| 
						 | 
					382c5c55ec | ||
| 
						 | 
					afb2306904 | ||
| 
						 | 
					2642da3be3 | ||
| 
						 | 
					dcbf283c9d | ||
| 
						 | 
					f38fa0132c | ||
| 
						 | 
					569053f7e0 | ||
| 
						 | 
					037a97ff3d | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -14,6 +14,7 @@ project/plugins/project/
 | 
				
			|||||||
.classpath
 | 
					.classpath
 | 
				
			||||||
.project
 | 
					.project
 | 
				
			||||||
.cache
 | 
					.cache
 | 
				
			||||||
 | 
					.settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# IntelliJ specific
 | 
					# IntelliJ specific
 | 
				
			||||||
.idea/
 | 
					.idea/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								README.md
									
									
									
									
									
								
							@@ -1,12 +1,15 @@
 | 
				
			|||||||
GitBucket
 | 
					GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
 | 
				
			||||||
=========
 | 
					=========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GitBucket is the easily installable Github clone written with Scala.
 | 
					GitBucket is the easily installable Github clone written with Scala.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Features
 | 
				
			||||||
 | 
					--------
 | 
				
			||||||
The current version of GitBucket provides a basic features below:
 | 
					The current version of GitBucket provides a basic features below:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Public / Private Git repository (http access only)
 | 
					- Public / Private Git repository (http and ssh access)
 | 
				
			||||||
- Repository viewer (some advanced features such as online file editing are not implemented)
 | 
					- Repository viewer and online file editing
 | 
				
			||||||
- Repository search (Code and Issues)
 | 
					- Repository search (Code and Issues)
 | 
				
			||||||
- Wiki
 | 
					- Wiki
 | 
				
			||||||
- Issues
 | 
					- Issues
 | 
				
			||||||
@@ -20,10 +23,9 @@ The current version of GitBucket provides a basic features below:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Following features are not implemented, but we will make them in the future release!
 | 
					Following features are not implemented, but we will make them in the future release!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- File editing in repository viewer
 | 
					 | 
				
			||||||
- Comment for the changeset
 | 
					- Comment for the changeset
 | 
				
			||||||
- Network graph
 | 
					- Network graph
 | 
				
			||||||
- Statics
 | 
					- Statistics
 | 
				
			||||||
- Watch / Star
 | 
					- Watch / Star
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
 | 
					If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
 | 
				
			||||||
@@ -35,23 +37,131 @@ Installation
 | 
				
			|||||||
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
 | 
					2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
 | 
				
			||||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
 | 
					3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The default administrator account is **root** and password is **root**.
 | 
					The default administrator account is **root** and password is **root**.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
 | 
					or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- --port=[NUMBER]
 | 
					- --port=[NUMBER]
 | 
				
			||||||
- --prefix=[CONTEXTPATH]
 | 
					- --prefix=[CONTEXTPATH]
 | 
				
			||||||
 | 
					- --host=[HOSTNAME]
 | 
				
			||||||
 | 
					- --gitbucket.home=[DATA_DIR]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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.
 | 
					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)
 | 
					For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Mac OS X
 | 
				
			||||||
 | 
					#### Installing Via Homebrew
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $ brew install gitbucket
 | 
				
			||||||
 | 
					    ==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
 | 
				
			||||||
 | 
					    ######################################################################## 100.0%
 | 
				
			||||||
 | 
					    ==> Caveats
 | 
				
			||||||
 | 
					    Note: When using launchctl the port will be 8080.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    To have launchd start gitbucket at login:
 | 
				
			||||||
 | 
					        ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
 | 
				
			||||||
 | 
					    Then to load gitbucket now:
 | 
				
			||||||
 | 
					        launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
 | 
				
			||||||
 | 
					    Or, if you don't want/need launchctl, you can just run:
 | 
				
			||||||
 | 
					        java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
 | 
				
			||||||
 | 
					    ==> Summary
 | 
				
			||||||
 | 
					    /usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Manual Installation
 | 
				
			||||||
 | 
					On OS X, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Run the following commands in `Terminal` to
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
 | 
				
			||||||
 | 
					- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Release Notes
 | 
					Release Notes
 | 
				
			||||||
--------
 | 
					--------
 | 
				
			||||||
 | 
					### 2.3 - 1 Sep 2014
 | 
				
			||||||
 | 
					- Scala based plugin system
 | 
				
			||||||
 | 
					- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
 | 
				
			||||||
 | 
					- Some bug fix and improvements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2.2.1 - 5 Aug 2014
 | 
				
			||||||
 | 
					- Bug fix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2.2 - 4 Aug 2014
 | 
				
			||||||
 | 
					- Plug-in system is available
 | 
				
			||||||
 | 
					- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
 | 
				
			||||||
 | 
					- tar.gz export for repository contents
 | 
				
			||||||
 | 
					- LDAP authentication improvement (mail address became optional)
 | 
				
			||||||
 | 
					- Show news feed of a private repository to members
 | 
				
			||||||
 | 
					- Some bug fix and improvements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2.1 - 6 Jul 2014
 | 
				
			||||||
 | 
					- Upgrade to Slick 2.0 from 1.9
 | 
				
			||||||
 | 
					- Base part of the plug-in system is merged
 | 
				
			||||||
 | 
					- Many bug fix and improvements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2.0 - 31 May 2014
 | 
				
			||||||
 | 
					- Modern Github UI
 | 
				
			||||||
 | 
					- Preview in AceEditor
 | 
				
			||||||
 | 
					- Select lines by clicking line number in blob view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.13 - 29 Apr 2014
 | 
				
			||||||
 | 
					- Direct file editing in the repository viewer using AceEditor
 | 
				
			||||||
 | 
					- File attachment for issues
 | 
				
			||||||
 | 
					- Atom feed of user activity
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.12 - 29 Mar 2014
 | 
				
			||||||
 | 
					- SSH repository access is available
 | 
				
			||||||
 | 
					- Allow users can create and management their groups
 | 
				
			||||||
 | 
					- Git submodule support
 | 
				
			||||||
 | 
					- Close issues via commit messages
 | 
				
			||||||
 | 
					- Show repository description below the name on repository page
 | 
				
			||||||
 | 
					- Fix presentation of the source viewer
 | 
				
			||||||
 | 
					- Upgrade to sbt 0.13
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.11.1 - 06 Mar 2014
 | 
				
			||||||
 | 
					- Bug fix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.11 - 01 Mar 2014
 | 
				
			||||||
 | 
					- Base URL for redirection, notification and repository URL box is configurable
 | 
				
			||||||
 | 
					- Remove ```--https``` option because it's possible to substitute in the base url
 | 
				
			||||||
 | 
					- Headline anchor is available for Markdown contents such as Wiki page
 | 
				
			||||||
 | 
					- Improve H2 connectivity
 | 
				
			||||||
 | 
					- Label is available for pull requests not only issues
 | 
				
			||||||
 | 
					- Delete branch button is added
 | 
				
			||||||
 | 
					- Repository icons are updated
 | 
				
			||||||
 | 
					- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
 | 
				
			||||||
 | 
					- Display reference to issue from others in comment list
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.10 - 01 Feb 2014
 | 
				
			||||||
 | 
					- Rename repository
 | 
				
			||||||
 | 
					- Transfer repository owner
 | 
				
			||||||
 | 
					- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
 | 
				
			||||||
 | 
					- Add LDAP display name attribute
 | 
				
			||||||
 | 
					- Response performance improvement
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.9 - 28 Dec 2013
 | 
				
			||||||
 | 
					- Display GITBUCKET_HOME on the system settings page
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1.8 - 30 Nov 2013
 | 
				
			||||||
 | 
					- Add user and group deletion
 | 
				
			||||||
 | 
					- Improve pull request performance
 | 
				
			||||||
 | 
					- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
 | 
				
			||||||
 | 
					- LDAP StartTLS support
 | 
				
			||||||
 | 
					- Enable hard wrapping in Markdown
 | 
				
			||||||
 | 
					- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
 | 
				
			||||||
 | 
					- Fix some bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 1.7 - 26 Oct 2013
 | 
					### 1.7 - 26 Oct 2013
 | 
				
			||||||
- Support working on Java6 in embedded Jetty mode
 | 
					- Support working on Java6 in embedded Jetty mode
 | 
				
			||||||
- Add ```--host``` option to bind specified host name in embedded Jetty mode
 | 
					- Add `--host` option to bind specified host name in embedded Jetty mode
 | 
				
			||||||
- Add ```--https=true``` option to use https in embedded Jetty mode
 | 
					- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
 | 
				
			||||||
- Add full name as user property
 | 
					- Add full name as user property
 | 
				
			||||||
- Change link color for absent Wiki pages
 | 
					- Change link color for absent Wiki pages
 | 
				
			||||||
- Add ZIP download button to the repository viewer tab
 | 
					- Add ZIP download button to the repository viewer tab
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
  <property name="target.dir" value="target"/>
 | 
					  <property name="target.dir" value="target"/>
 | 
				
			||||||
  <property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
 | 
					  <property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
 | 
				
			||||||
  <property name="jetty.dir" value="embed-jetty"/>
 | 
					  <property name="jetty.dir" value="embed-jetty"/>
 | 
				
			||||||
  <property name="scala.version" value="2.10"/>
 | 
					  <property name="scala.version" value="2.11"/>
 | 
				
			||||||
  <property name="gitbucket.version" value="0.0.1"/>
 | 
					  <property name="gitbucket.version" value="0.0.1"/>
 | 
				
			||||||
  <property name="jetty.version" value="8.1.8.v20121106"/>
 | 
					  <property name="jetty.version" value="8.1.8.v20121106"/>
 | 
				
			||||||
  <property name="servlet.version" value="3.0.0.v201112011016"/>
 | 
					  <property name="servlet.version" value="3.0.0.v201112011016"/>
 | 
				
			||||||
@@ -50,8 +50,8 @@
 | 
				
			|||||||
  </target>
 | 
					  </target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <target name="rename" depends="embed">
 | 
					  <target name="rename" depends="embed">
 | 
				
			||||||
    <rename src="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
 | 
					    <move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
 | 
				
			||||||
            dest="${target.dir}/scala-${scala.version}/gitbucket.war"/>
 | 
					            tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
 | 
				
			||||||
  </target>
 | 
					  </target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <target name="all" depends="rename">
 | 
					  <target name="all" depends="rename">
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					# Contrib Notes #
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The configuration script adapts according to the OS.
 | 
				
			||||||
 | 
					The `linux` directory contains scripts for Ubuntu and RedHat.
 | 
				
			||||||
 | 
					The Mac scripts have been folded in as well.
 | 
				
			||||||
 | 
					Common scripts are in this directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To run:
 | 
				
			||||||
 | 
					1. Edit `gitbucket.conf` to suit.
 | 
				
			||||||
 | 
					2. Type: `install`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										62
									
								
								contrib/gitbucket.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								contrib/gitbucket.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					# Configuration section is below. Ignore this part
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isUbuntu {
 | 
				
			||||||
 | 
					  if [ -f /etc/lsb-release ]; then
 | 
				
			||||||
 | 
					    grep -i ubuntu /etc/lsb-release | head -n 1 | cut -d \  -f 1 | cut -d = -f 2
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isRedHat {
 | 
				
			||||||
 | 
					  if [ -d "/etc/rc.d/init.d" ]; then echo yes; fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isMac {
 | 
				
			||||||
 | 
					  if [[ "$(uname -a | cut -d \  -f 1 )" == "Darwin" ]]; then echo yes; fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 
 | 
				
			||||||
 | 
					# Configuration section start
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Bind host
 | 
				
			||||||
 | 
					GITBUCKET_HOST=0.0.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Other Java option
 | 
				
			||||||
 | 
					GITBUCKET_JVM_OPTS=-Dmail.smtp.starttls.enable=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Data directory, holds repositories
 | 
				
			||||||
 | 
					GITBUCKET_HOME=/var/lib/gitbucket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GITBUCKET_LOG_DIR=/var/log/gitbucket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Server port
 | 
				
			||||||
 | 
					GITBUCKET_PORT=8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
 | 
				
			||||||
 | 
					GITBUCKET_PREFIX=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Directory where GitBucket is installed
 | 
				
			||||||
 | 
					# Configuration is stored here:
 | 
				
			||||||
 | 
					GITBUCKET_DIR=/usr/share/gitbucket
 | 
				
			||||||
 | 
					GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Path to the WAR file
 | 
				
			||||||
 | 
					GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# GitBucket version to fetch when installing
 | 
				
			||||||
 | 
					GITBUCKET_VERSION=2.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# End of configuration section. Ignore this part
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					if [ `isUbuntu` ]; then
 | 
				
			||||||
 | 
					  GITBUCKET_SERVICE=/etc/init.d/gitbucket
 | 
				
			||||||
 | 
					elif [ `isRedHat` ]; then
 | 
				
			||||||
 | 
					  GITBUCKET_SERVICE=/etc/rc.d/init.d
 | 
				
			||||||
 | 
					elif [ `isMac` ]; then
 | 
				
			||||||
 | 
					  GITBUCKET_SERVICE=/Library/StartupItems/GitBucket/GitBucket
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					  echo "Don't know how to install onto this OS"
 | 
				
			||||||
 | 
					  exit -2
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					#!/bin/bash
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# /etc/rc.d/init.d/gitbucket
 | 
					# RedHat: /etc/rc.d/init.d/gitbucket
 | 
				
			||||||
 | 
					# Ubuntu: /etc/init.d/gitbucket
 | 
				
			||||||
 | 
					# Mac OS/X: /Library/StartupItems/GitBucket
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Starts the GitBucket server
 | 
					# Starts the GitBucket server
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
@@ -8,28 +10,44 @@
 | 
				
			|||||||
# description: Run GitBucket server
 | 
					# description: Run GitBucket server
 | 
				
			||||||
# processname: java
 | 
					# processname: java
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Source function library
 | 
					set -e
 | 
				
			||||||
. /etc/rc.d/init.d/functions
 | 
					
 | 
				
			||||||
 | 
					[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions  # RedHat
 | 
				
			||||||
 | 
					[ -f /etc/rc.common ] && source /etc/rc.common # Mac OS/X
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Default values
 | 
					# Default values
 | 
				
			||||||
GITBUCKET_HOME=/var/lib/gitbucket
 | 
					GITBUCKET_HOME=/var/lib/gitbucket
 | 
				
			||||||
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
 | 
					GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Pull in cq settings
 | 
					# Pull in cq settings
 | 
				
			||||||
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
 | 
					[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
 | 
				
			||||||
 | 
					[ -f gitbucket.conf ] && source gitbucket.conf  # For all systems
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Location of the log and PID file
 | 
					# Location of the log and PID file
 | 
				
			||||||
LOG_FILE=/var/log/gitbucket/run.log
 | 
					LOG_FILE=$GITBUCKET_LOG_DIR/run.log
 | 
				
			||||||
PID_FILE=/var/run/gitbucket.pid
 | 
					PID_FILE=/var/run/gitbucket.pid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Default return value
 | 
					RED='\033[1m\E[37;41m'
 | 
				
			||||||
RETVAL=0
 | 
					GREEN='\033[1m\E[37;42m'
 | 
				
			||||||
 | 
					OFF='\E[0m'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$(which success)" ]; then
 | 
				
			||||||
 | 
						function success {
 | 
				
			||||||
 | 
						  printf "%b\n" "$GREEN $* $OFF"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					if [ -z "$(which failure)" ]; then
 | 
				
			||||||
 | 
						function failure {
 | 
				
			||||||
 | 
						  printf "%b\n" "$RED $* $OFF"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RETVAL=0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
start() {
 | 
					start() {
 | 
				
			||||||
	echo -n $"Starting GitBucket server: "
 | 
						echo -n $"Starting GitBucket server: "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Compile statup parameters
 | 
					  START_OPTS=
 | 
				
			||||||
	if [ $GITBUCKET_PORT ]; then
 | 
						if [ $GITBUCKET_PORT ]; then
 | 
				
			||||||
		START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
 | 
							START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
 | 
				
			||||||
	fi
 | 
						fi
 | 
				
			||||||
@@ -39,21 +57,16 @@ start() {
 | 
				
			|||||||
	if [ $GITBUCKET_HOST ]; then
 | 
						if [ $GITBUCKET_HOST ]; then
 | 
				
			||||||
		START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
 | 
							START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
 | 
				
			||||||
	fi
 | 
						fi
 | 
				
			||||||
	if [ $GITBUCKET_HTTPS ]; then
 | 
					 | 
				
			||||||
		START_OPTS="${START_OPTS} --https=true"
 | 
					 | 
				
			||||||
	fi
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Run the Java process
 | 
					 | 
				
			||||||
	GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
 | 
						GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
 | 
				
			||||||
	RETVAL=$?
 | 
						RETVAL=$?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Store PID of the Java process into a file
 | 
					 | 
				
			||||||
	echo $! > $PID_FILE
 | 
						echo $! > $PID_FILE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if [ $RETVAL -eq 0 ] ; then
 | 
						if [ $RETVAL -eq 0 ] ; then
 | 
				
			||||||
		success "GitBucket startup"
 | 
							success "Success"
 | 
				
			||||||
	else
 | 
						else
 | 
				
			||||||
		failure "GitBucket startup"
 | 
							failure "Exit code $RETVAL"
 | 
				
			||||||
	fi
 | 
						fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	echo
 | 
						echo
 | 
				
			||||||
@@ -85,25 +98,41 @@ restart() {
 | 
				
			|||||||
	start
 | 
						start
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## MacOS proxies for System V service hooks:
 | 
				
			||||||
 | 
					StartService() {
 | 
				
			||||||
 | 
						start
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case "$1" in
 | 
					StopService() {
 | 
				
			||||||
start)
 | 
						stop
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RestartService() {
 | 
				
			||||||
 | 
						restart
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ `isMac` ]; then
 | 
				
			||||||
 | 
					  RunService "$1"
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
						case "$1" in
 | 
				
			||||||
 | 
							start)
 | 
				
			||||||
			start
 | 
								start
 | 
				
			||||||
			;;
 | 
								;;
 | 
				
			||||||
stop)
 | 
							stop)
 | 
				
			||||||
			stop
 | 
								stop
 | 
				
			||||||
			;;
 | 
								;;
 | 
				
			||||||
restart)
 | 
							restart)
 | 
				
			||||||
			restart
 | 
								restart
 | 
				
			||||||
			;;
 | 
								;;
 | 
				
			||||||
status)
 | 
							status)
 | 
				
			||||||
			status -p $PID_FILE java
 | 
								status -p $PID_FILE java
 | 
				
			||||||
			RETVAL=$?
 | 
								RETVAL=$?
 | 
				
			||||||
			;;
 | 
								;;
 | 
				
			||||||
*)
 | 
							*)
 | 
				
			||||||
			echo $"Usage: $0 [start|stop|restart|status]"
 | 
								echo $"Usage: $0 [start|stop|restart|status]"
 | 
				
			||||||
			RETVAL=2
 | 
								RETVAL=2
 | 
				
			||||||
esac
 | 
						esac
 | 
				
			||||||
 | 
					  exit $RETVAL
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
exit $RETVAL
 | 
					 | 
				
			||||||
							
								
								
									
										69
									
								
								contrib/install
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										69
									
								
								contrib/install
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Only tested on Ubuntu 14.04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Uses information stored in GitBucket git repo on GitHub as defaults.
 | 
				
			||||||
 | 
					# Edit gitbucket.conf before running this
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GITBUCKET_VERSION=2.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ ! -f gitbucket.conf ]; then
 | 
				
			||||||
 | 
					  echo "gitbucket.conf not found, aborting"
 | 
				
			||||||
 | 
					  exit -3
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					source gitbucket.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createDir {
 | 
				
			||||||
 | 
					  if [ ! -d "$1" ]; then
 | 
				
			||||||
 | 
					    echo "Making $1 directory."
 | 
				
			||||||
 | 
					    sudo mkdir -p "$1"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$(which iptables)" ]; then
 | 
				
			||||||
 | 
					  echo "Opening port $GITBUCKET_PORT in firewall."
 | 
				
			||||||
 | 
					  sudo iptables -A INPUT -p tcp --dport $GITBUCKET_PORT -j ACCEPT
 | 
				
			||||||
 | 
					  echo "Please use iptables-persistent:"
 | 
				
			||||||
 | 
					  echo "  sudo apt-get install iptables-persistent"
 | 
				
			||||||
 | 
					  echo "After installed, you can save/reload iptables rules anytime:"
 | 
				
			||||||
 | 
					  echo "  sudo /etc/init.d/iptables-persistent save"
 | 
				
			||||||
 | 
					  echo "  sudo /etc/init.d/iptables-persistent reload"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					createDir "$GITBUCKET_HOME"
 | 
				
			||||||
 | 
					createDir "$GITBUCKET_WAR_DIR"
 | 
				
			||||||
 | 
					createDir "$GITBUCKET_DIR"
 | 
				
			||||||
 | 
					createDir "$GITBUCKET_LOG_DIR"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
 | 
				
			||||||
 | 
					sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/takezoe/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Copying gitbucket.conf to $GITBUCKET_DIR"
 | 
				
			||||||
 | 
					sudo cp gitbucket.conf $GITBUCKET_DIR
 | 
				
			||||||
 | 
					if [ `isUbuntu` ] || [ `isRedHat` ]; then
 | 
				
			||||||
 | 
					  sudo cp gitbucket.init "$GITBUCKET_SERVICE"
 | 
				
			||||||
 | 
					  # Install gitbucket as a service that starts when system boots
 | 
				
			||||||
 | 
					  sudo chown root:root $GITBUCKET_SERVICE
 | 
				
			||||||
 | 
					  sudo chmod 755 $GITBUCKET_SERVICE
 | 
				
			||||||
 | 
					  sudo update-rc.d "$(basename $GITBUCKET_SERVICE)" defaults 98 02
 | 
				
			||||||
 | 
					  echo "Starting GitBucket service"
 | 
				
			||||||
 | 
					  sudo $GITBUCKET_SERVICE start
 | 
				
			||||||
 | 
					elif [ `isMac` ]; then
 | 
				
			||||||
 | 
					  sudo macosx/makePlist
 | 
				
			||||||
 | 
					  echo "Starting GitBucket service"
 | 
				
			||||||
 | 
					  sudo cp gitbucket.conf "$GITBUCKET_SERVICE"
 | 
				
			||||||
 | 
					  sudo cp gitbucket.init "$GITBUCKET_SERVICE"
 | 
				
			||||||
 | 
					  sudo chmod a+x "$GITBUCKET_SERVICE"
 | 
				
			||||||
 | 
					  sudo "$GITBUCKET_SERVICE" start
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					  echo "Don't know how to install this OS"
 | 
				
			||||||
 | 
					  exit -2
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ $? != 0 ]; then
 | 
				
			||||||
 | 
					  less "$GITBUCKET_LOG_DIR/run.log"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
Name:		gitbucket
 | 
					Name:		gitbucket
 | 
				
			||||||
Summary:	Github clone written with Scala.
 | 
					Summary:	GitHub clone written with Scala.
 | 
				
			||||||
Version:	1.6
 | 
					Version:	1.7
 | 
				
			||||||
Release:	1%{?dist}
 | 
					Release:	1%{?dist}
 | 
				
			||||||
License:	Apache
 | 
					License:	Apache
 | 
				
			||||||
URL:		https://github.com/takezoe/gitbucket
 | 
					URL:		https://github.com/takezoe/gitbucket
 | 
				
			||||||
@@ -15,7 +15,7 @@ Requires:	java >= 1.7
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
%description
 | 
					%description
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GitBucket is the easily installable Github clone written with Scala.
 | 
					GitBucket is the easily installable GitHub clone written with Scala.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%install
 | 
					%install
 | 
				
			||||||
@@ -40,5 +40,8 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%changelog
 | 
					%changelog
 | 
				
			||||||
 | 
					* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
 | 
				
			||||||
 | 
					- Version bump to v1.7.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
 | 
					* Thu Oct 17 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
 | 
				
			||||||
- First build.
 | 
					- First build.
 | 
				
			||||||
							
								
								
									
										28
									
								
								contrib/macosx/makePlist
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										28
									
								
								contrib/macosx/makePlist
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# From http://docstore.mik.ua/orelly/unix3/mac/ch02_02.htm
 | 
				
			||||||
 | 
					source gitbucket.conf
 | 
				
			||||||
 | 
					GITBUCKET_SERVICE_DIR=`dirname "$GITBUCKET_SERVICE"`
 | 
				
			||||||
 | 
					mkdir -p "$GITBUCKET_SERVICE_DIR"
 | 
				
			||||||
 | 
					cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
				
			||||||
 | 
					<plist version="1.0">
 | 
				
			||||||
 | 
					  <dict>
 | 
				
			||||||
 | 
					    <key>Label</key>
 | 
				
			||||||
 | 
					    <string>gitbucket</string>
 | 
				
			||||||
 | 
					    <key>ProgramArguments</key>
 | 
				
			||||||
 | 
					    <array>
 | 
				
			||||||
 | 
					      <string>/usr/bin/java</string>
 | 
				
			||||||
 | 
					      <string>$GITBUCKET_JVM_OPTS</string>
 | 
				
			||||||
 | 
					      <string>-jar</string>
 | 
				
			||||||
 | 
					      <string>gitbucket.war</string>
 | 
				
			||||||
 | 
					      <string>--host=$GITBUCKET_HOST</string>
 | 
				
			||||||
 | 
					      <string>--port=$GITBUCKET_PORT</string>
 | 
				
			||||||
 | 
					      <string>--https=true</string>
 | 
				
			||||||
 | 
					    </array>
 | 
				
			||||||
 | 
					    <key>RunAtLoad</key>
 | 
				
			||||||
 | 
					    <true/>
 | 
				
			||||||
 | 
					  </dict>
 | 
				
			||||||
 | 
					</plist>
 | 
				
			||||||
 | 
					EOF
 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
# Bind host
 | 
					 | 
				
			||||||
#GITBUCKET_HOST=0.0.0.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Server port
 | 
					 | 
				
			||||||
#GITBUCKET_PORT=8080
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Force HTTPS scheme
 | 
					 | 
				
			||||||
#GITBUCKET_HTTPS=false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Data directory (GITBUCKET_HOME/gitbucket)
 | 
					 | 
				
			||||||
#GITBUCKET_HOME=/var/lib/gitbucket
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Path to the WAR file
 | 
					 | 
				
			||||||
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
 | 
					 | 
				
			||||||
#GITBUCKET_PREFIX=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Other Java option
 | 
					 | 
				
			||||||
#GITBUCKET_JVM_OPTS=
 | 
					 | 
				
			||||||
							
								
								
									
										964
									
								
								etc/icons.svg
									
									
									
									
									
								
							
							
						
						
									
										964
									
								
								etc/icons.svg
									
									
									
									
									
								
							@@ -2,6 +2,7 @@
 | 
				
			|||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
					<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svg
 | 
					<svg
 | 
				
			||||||
 | 
					   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
 | 
				
			||||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
					   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
				
			||||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
					   xmlns:cc="http://creativecommons.org/ns#"
 | 
				
			||||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
					   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
				
			||||||
@@ -16,7 +17,16 @@
 | 
				
			|||||||
   inkscape:version="0.48.4 r9939"
 | 
					   inkscape:version="0.48.4 r9939"
 | 
				
			||||||
   sodipodi:docname="icons.svg">
 | 
					   sodipodi:docname="icons.svg">
 | 
				
			||||||
  <defs
 | 
					  <defs
 | 
				
			||||||
     id="defs4" />
 | 
					     id="defs4">
 | 
				
			||||||
 | 
					    <linearGradient
 | 
				
			||||||
 | 
					       id="linearGradient4044"
 | 
				
			||||||
 | 
					       osb:paint="solid">
 | 
				
			||||||
 | 
					      <stop
 | 
				
			||||||
 | 
					         style="stop-color:#000000;stop-opacity:1;"
 | 
				
			||||||
 | 
					         offset="0"
 | 
				
			||||||
 | 
					         id="stop4046" />
 | 
				
			||||||
 | 
					    </linearGradient>
 | 
				
			||||||
 | 
					  </defs>
 | 
				
			||||||
  <sodipodi:namedview
 | 
					  <sodipodi:namedview
 | 
				
			||||||
     id="base"
 | 
					     id="base"
 | 
				
			||||||
     pagecolor="#ffffff"
 | 
					     pagecolor="#ffffff"
 | 
				
			||||||
@@ -24,18 +34,18 @@
 | 
				
			|||||||
     borderopacity="1.0"
 | 
					     borderopacity="1.0"
 | 
				
			||||||
     inkscape:pageopacity="0.0"
 | 
					     inkscape:pageopacity="0.0"
 | 
				
			||||||
     inkscape:pageshadow="2"
 | 
					     inkscape:pageshadow="2"
 | 
				
			||||||
     inkscape:zoom="1.4"
 | 
					     inkscape:zoom="0.7"
 | 
				
			||||||
     inkscape:cx="629.30023"
 | 
					     inkscape:cx="482.58197"
 | 
				
			||||||
     inkscape:cy="281.44758"
 | 
					     inkscape:cy="-83.92636"
 | 
				
			||||||
     inkscape:document-units="px"
 | 
					     inkscape:document-units="px"
 | 
				
			||||||
     inkscape:current-layer="layer1-9"
 | 
					     inkscape:current-layer="layer1-9"
 | 
				
			||||||
     showgrid="false"
 | 
					     showgrid="false"
 | 
				
			||||||
     inkscape:window-width="1366"
 | 
					     inkscape:window-width="1366"
 | 
				
			||||||
     inkscape:window-height="705"
 | 
					     inkscape:window-height="715"
 | 
				
			||||||
     inkscape:window-x="-8"
 | 
					     inkscape:window-x="-8"
 | 
				
			||||||
     inkscape:window-y="-8"
 | 
					     inkscape:window-y="-8"
 | 
				
			||||||
     inkscape:window-maximized="1"
 | 
					     inkscape:window-maximized="1"
 | 
				
			||||||
     inkscape:snap-global="true"
 | 
					     inkscape:snap-global="false"
 | 
				
			||||||
     inkscape:snap-grids="false"
 | 
					     inkscape:snap-grids="false"
 | 
				
			||||||
     inkscape:snap-page="false"
 | 
					     inkscape:snap-page="false"
 | 
				
			||||||
     inkscape:snap-bbox="true"
 | 
					     inkscape:snap-bbox="true"
 | 
				
			||||||
@@ -746,6 +756,948 @@
 | 
				
			|||||||
         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"
 | 
					         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"
 | 
					         id="rect2995-0-2-7-7"
 | 
				
			||||||
         inkscape:connector-curvature="0" />
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="rect3083"
 | 
				
			||||||
 | 
					         width="170.93134"
 | 
				
			||||||
 | 
					         height="207.72536"
 | 
				
			||||||
 | 
					         x="38.526306"
 | 
				
			||||||
 | 
					         y="1299.8645" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="rect3083-7"
 | 
				
			||||||
 | 
					         width="171.86089"
 | 
				
			||||||
 | 
					         height="167.53221"
 | 
				
			||||||
 | 
					         x="38.061527"
 | 
				
			||||||
 | 
					         y="1300.4821" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4"
 | 
				
			||||||
 | 
					         y="1301.3412"
 | 
				
			||||||
 | 
					         x="42.553577"
 | 
				
			||||||
 | 
					         height="163.64935"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-0"
 | 
				
			||||||
 | 
					         y="1321.9025"
 | 
				
			||||||
 | 
					         x="85.732407"
 | 
				
			||||||
 | 
					         height="17.555511"
 | 
				
			||||||
 | 
					         width="16.782965"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-0-9"
 | 
				
			||||||
 | 
					         y="1356.7848"
 | 
				
			||||||
 | 
					         x="85.732407"
 | 
				
			||||||
 | 
					         height="17.555511"
 | 
				
			||||||
 | 
					         width="16.782965"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-0-9-4"
 | 
				
			||||||
 | 
					         y="1391.6671"
 | 
				
			||||||
 | 
					         x="85.732407"
 | 
				
			||||||
 | 
					         height="17.555511"
 | 
				
			||||||
 | 
					         width="16.782965"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-0-9-4-8"
 | 
				
			||||||
 | 
					         y="1426.5494"
 | 
				
			||||||
 | 
					         x="85.732407"
 | 
				
			||||||
 | 
					         height="17.555511"
 | 
				
			||||||
 | 
					         width="16.782965"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-8"
 | 
				
			||||||
 | 
					         y="1482.7141"
 | 
				
			||||||
 | 
					         x="70.149086"
 | 
				
			||||||
 | 
					         height="30.541632"
 | 
				
			||||||
 | 
					         width="42.755199"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="star"
 | 
				
			||||||
 | 
					         style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="path4002"
 | 
				
			||||||
 | 
					         sodipodi:sides="3"
 | 
				
			||||||
 | 
					         sodipodi:cx="235.71429"
 | 
				
			||||||
 | 
					         sodipodi:cy="1000.2193"
 | 
				
			||||||
 | 
					         sodipodi:r1="15.016997"
 | 
				
			||||||
 | 
					         sodipodi:r2="7.5084987"
 | 
				
			||||||
 | 
					         sodipodi:arg1="0"
 | 
				
			||||||
 | 
					         sodipodi:arg2="1.0471976"
 | 
				
			||||||
 | 
					         inkscape:flatsided="false"
 | 
				
			||||||
 | 
					         inkscape:rounded="0"
 | 
				
			||||||
 | 
					         inkscape:randomized="0"
 | 
				
			||||||
 | 
					         d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
 | 
				
			||||||
 | 
					         transform="matrix(1.0346242,0,0,1.5150471,-165.95814,-2.7851671)"
 | 
				
			||||||
 | 
					         inkscape:transform-center-x="-2.5637799" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="star"
 | 
				
			||||||
 | 
					         style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="path4002-2"
 | 
				
			||||||
 | 
					         sodipodi:sides="3"
 | 
				
			||||||
 | 
					         sodipodi:cx="235.71429"
 | 
				
			||||||
 | 
					         sodipodi:cy="1000.2193"
 | 
				
			||||||
 | 
					         sodipodi:r1="15.016997"
 | 
				
			||||||
 | 
					         sodipodi:r2="7.5084987"
 | 
				
			||||||
 | 
					         sodipodi:arg1="0"
 | 
				
			||||||
 | 
					         sodipodi:arg2="1.0471976"
 | 
				
			||||||
 | 
					         inkscape:flatsided="false"
 | 
				
			||||||
 | 
					         inkscape:rounded="0"
 | 
				
			||||||
 | 
					         inkscape:randomized="0"
 | 
				
			||||||
 | 
					         d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
 | 
				
			||||||
 | 
					         transform="matrix(-0.93510984,0,0,1.5150471,326.24502,-2.7851671)"
 | 
				
			||||||
 | 
					         inkscape:transform-center-x="3.5106467" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:9.34194565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="rect3083-4"
 | 
				
			||||||
 | 
					         width="170.93134"
 | 
				
			||||||
 | 
					         height="207.72536"
 | 
				
			||||||
 | 
					         x="280.50113"
 | 
				
			||||||
 | 
					         y="1299.152" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="color:#000000;fill:none;stroke:#b3b3b3;stroke-width:8.41239071;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="rect3083-7-5"
 | 
				
			||||||
 | 
					         width="171.86087"
 | 
				
			||||||
 | 
					         height="167.53221"
 | 
				
			||||||
 | 
					         x="280.03638"
 | 
				
			||||||
 | 
					         y="1299.7695" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5"
 | 
				
			||||||
 | 
					         y="1300.6287"
 | 
				
			||||||
 | 
					         x="284.52841"
 | 
				
			||||||
 | 
					         height="163.64934"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-8-5"
 | 
				
			||||||
 | 
					         y="1482.0016"
 | 
				
			||||||
 | 
					         x="312.12393"
 | 
				
			||||||
 | 
					         height="30.541632"
 | 
				
			||||||
 | 
					         width="42.755199"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="star"
 | 
				
			||||||
 | 
					         style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="path4002-27"
 | 
				
			||||||
 | 
					         sodipodi:sides="3"
 | 
				
			||||||
 | 
					         sodipodi:cx="235.71429"
 | 
				
			||||||
 | 
					         sodipodi:cy="1000.2193"
 | 
				
			||||||
 | 
					         sodipodi:r1="15.016997"
 | 
				
			||||||
 | 
					         sodipodi:r2="7.5084987"
 | 
				
			||||||
 | 
					         sodipodi:arg1="0"
 | 
				
			||||||
 | 
					         sodipodi:arg2="1.0471976"
 | 
				
			||||||
 | 
					         inkscape:flatsided="false"
 | 
				
			||||||
 | 
					         inkscape:rounded="0"
 | 
				
			||||||
 | 
					         inkscape:randomized="0"
 | 
				
			||||||
 | 
					         d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
 | 
				
			||||||
 | 
					         transform="matrix(1.0346242,0,0,1.5150471,76.016678,-3.496726)"
 | 
				
			||||||
 | 
					         inkscape:transform-center-x="-3.8842459"
 | 
				
			||||||
 | 
					         inkscape:transform-center-y="-1.5464308e-005" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="star"
 | 
				
			||||||
 | 
					         style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:7.55999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
 | 
				
			||||||
 | 
					         id="path4002-2-6"
 | 
				
			||||||
 | 
					         sodipodi:sides="3"
 | 
				
			||||||
 | 
					         sodipodi:cx="235.71429"
 | 
				
			||||||
 | 
					         sodipodi:cy="1000.2193"
 | 
				
			||||||
 | 
					         sodipodi:r1="15.016997"
 | 
				
			||||||
 | 
					         sodipodi:r2="7.5084987"
 | 
				
			||||||
 | 
					         sodipodi:arg1="0"
 | 
				
			||||||
 | 
					         sodipodi:arg2="1.0471976"
 | 
				
			||||||
 | 
					         inkscape:flatsided="false"
 | 
				
			||||||
 | 
					         inkscape:rounded="0"
 | 
				
			||||||
 | 
					         inkscape:randomized="0"
 | 
				
			||||||
 | 
					         d="m 250.73129,1000.2193 -11.26275,6.5026 -11.26274,6.5025 0,-13.0051 0,-13.0051 11.26274,6.50255 z"
 | 
				
			||||||
 | 
					         transform="matrix(-0.93510984,0,0,1.5150471,568.21986,-3.496726)"
 | 
				
			||||||
 | 
					         inkscape:transform-center-x="5.318797"
 | 
				
			||||||
 | 
					         inkscape:transform-center-y="-1.5464308e-005" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-7"
 | 
				
			||||||
 | 
					         y="1392.2405"
 | 
				
			||||||
 | 
					         x="365.67133"
 | 
				
			||||||
 | 
					         height="58.049755"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-7-6"
 | 
				
			||||||
 | 
					         y="1319.5453"
 | 
				
			||||||
 | 
					         x="326.67615"
 | 
				
			||||||
 | 
					         height="49.632401"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-7-8"
 | 
				
			||||||
 | 
					         y="1179.0293"
 | 
				
			||||||
 | 
					         x="-767.54126"
 | 
				
			||||||
 | 
					         height="58.049755"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none"
 | 
				
			||||||
 | 
					         transform="matrix(0.68860063,-0.7251408,0.7251408,0.68860063,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-7-6-9"
 | 
				
			||||||
 | 
					         y="1319.5453"
 | 
				
			||||||
 | 
					         x="403.28595"
 | 
				
			||||||
 | 
					         height="49.632404"
 | 
				
			||||||
 | 
					         width="29.769083"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-7-8-2"
 | 
				
			||||||
 | 
					         y="623.14606"
 | 
				
			||||||
 | 
					         x="-1287.8975"
 | 
				
			||||||
 | 
					         height="55.681484"
 | 
				
			||||||
 | 
					         width="28.564859"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none"
 | 
				
			||||||
 | 
					         transform="matrix(-0.68607628,-0.72752961,-0.72274236,0.69111755,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="color:#000000;fill:#ffffff;stroke:#b3b3b3;stroke-width:7.29121827999999980;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;fill-opacity:1"
 | 
				
			||||||
 | 
					         id="rect3083-7-5-7"
 | 
				
			||||||
 | 
					         width="172.98204"
 | 
				
			||||||
 | 
					         height="125.03616"
 | 
				
			||||||
 | 
					         x="529.78156"
 | 
				
			||||||
 | 
					         y="1383.6165" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-9"
 | 
				
			||||||
 | 
					         y="1385.3533"
 | 
				
			||||||
 | 
					         x="663.37042"
 | 
				
			||||||
 | 
					         height="123.85819"
 | 
				
			||||||
 | 
					         width="38.18644"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-9-5"
 | 
				
			||||||
 | 
					         y="1401.4539"
 | 
				
			||||||
 | 
					         x="552.03174"
 | 
				
			||||||
 | 
					         height="15.96297"
 | 
				
			||||||
 | 
					         width="117.00352"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-9-5-4"
 | 
				
			||||||
 | 
					         y="1437.4023"
 | 
				
			||||||
 | 
					         x="551.16083"
 | 
				
			||||||
 | 
					         height="15.96297"
 | 
				
			||||||
 | 
					         width="117.00352"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-4-5-9-5-4-3"
 | 
				
			||||||
 | 
					         y="1473.7642"
 | 
				
			||||||
 | 
					         x="551.16083"
 | 
				
			||||||
 | 
					         height="15.96297"
 | 
				
			||||||
 | 
					         width="117.00352"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:none;stroke:#b3b3b3;stroke-width:23.0681076;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 558.62308,1380.7989 0,-45.237 c 0,0 13.52904,-35.6384 56.38304,-36.1894 40.81922,-0.5248 55.47363,34.6931 55.47363,34.6931 l 0.17276,48.4719"
 | 
				
			||||||
 | 
					         id="path4310"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         sodipodi:nodetypes="ccscc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2991-7-1-4"
 | 
				
			||||||
 | 
					         transform="translate(668.66057,1115.0272)"
 | 
				
			||||||
 | 
					         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-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,711.41194,1163.4493)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-2-8"
 | 
				
			||||||
 | 
					         y="1378.4849"
 | 
				
			||||||
 | 
					         x="916.58545"
 | 
				
			||||||
 | 
					         height="99.396141"
 | 
				
			||||||
 | 
					         width="20.706863"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 884.0251,1366.2678 -64.6851,-36.2114 10.70013,55.9569 53.98497,-19.7455 z"
 | 
				
			||||||
 | 
					         id="rect4046-3-4"
 | 
				
			||||||
 | 
					         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 873.36878,1359.3959 -43.65605,-24.4345 6.99871,38.1562 36.65734,-13.7217 z"
 | 
				
			||||||
 | 
					         id="rect4046-5"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:13.63542366;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 1162.2186,1316.0972 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6599,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1223,94.1223 23.3412,-23.3412 46.6824,-46.6824 70.0236,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
 | 
				
			||||||
 | 
					         id="rect3075-11"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-2-8-6"
 | 
				
			||||||
 | 
					         y="899.99463"
 | 
				
			||||||
 | 
					         x="-1417.3273"
 | 
				
			||||||
 | 
					         height="99.396141"
 | 
				
			||||||
 | 
					         width="20.706863"
 | 
				
			||||||
 | 
					         style="fill:#b3b3b3;stroke:none;stroke-width:0.93666755999999995"
 | 
				
			||||||
 | 
					         transform="matrix(0,-1,1,0,0,0)" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 1172.5522,1326.6744 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
 | 
				
			||||||
 | 
					         id="rect3075-11-7"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="arc"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#b3b3b3;stroke-width:6.57334423;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
 | 
				
			||||||
 | 
					         id="path3100-2"
 | 
				
			||||||
 | 
					         sodipodi:cx="700"
 | 
				
			||||||
 | 
					         sodipodi:cy="812.36218"
 | 
				
			||||||
 | 
					         sodipodi:rx="10"
 | 
				
			||||||
 | 
					         sodipodi:ry="10"
 | 
				
			||||||
 | 
					         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"
 | 
				
			||||||
 | 
					         transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-667.98357,1217.7251)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#b3b3b3;stroke-width:10.80681515;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect4114"
 | 
				
			||||||
 | 
					         width="45.086407"
 | 
				
			||||||
 | 
					         height="62.401226"
 | 
				
			||||||
 | 
					         x="-133.16023"
 | 
				
			||||||
 | 
					         y="1850.2394"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2991-7-6"
 | 
				
			||||||
 | 
					         transform="translate(1090.5728,-207.2632)"
 | 
				
			||||||
 | 
					         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:#a0a0a0;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2993-4-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:#808080;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc"
 | 
				
			||||||
 | 
					         transform="matrix(0.83611704,0,0,0.83611704,1133.3242,-158.84107)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-8"
 | 
				
			||||||
 | 
					         y="10.829478"
 | 
				
			||||||
 | 
					         x="1332.5247"
 | 
				
			||||||
 | 
					         height="99.221687"
 | 
				
			||||||
 | 
					         width="29.189819"
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;stroke:#ffffff;stroke-width:1.11112404000000000;fill-opacity:1" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2997-9-2"
 | 
				
			||||||
 | 
					         y="129.62337"
 | 
				
			||||||
 | 
					         x="1332.7828"
 | 
				
			||||||
 | 
					         height="26.258072"
 | 
				
			||||||
 | 
					         width="29.724136"
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;stroke:#ffffff;stroke-width:0.57680577000000000;fill-opacity:1" />
 | 
				
			||||||
 | 
					      <g
 | 
				
			||||||
 | 
					         id="g4284-1"
 | 
				
			||||||
 | 
					         transform="translate(670.07237,-816.24186)"
 | 
				
			||||||
 | 
					         style="stroke:#a0a0a0;stroke-opacity:1">
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           sodipodi:nodetypes="czcczcc"
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="rect4201-26"
 | 
				
			||||||
 | 
					           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:#a0a0a0;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1108.1473"
 | 
				
			||||||
 | 
					           x="597.4068"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.265846"
 | 
				
			||||||
 | 
					           id="rect4203-0"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1142.7776"
 | 
				
			||||||
 | 
					           x="598.48895"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.26585"
 | 
				
			||||||
 | 
					           id="rect4203-2-4"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1176.1093"
 | 
				
			||||||
 | 
					           x="598.48895"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.26585"
 | 
				
			||||||
 | 
					           id="rect4203-2-3-9"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           sodipodi:nodetypes="czc"
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="path4245-4"
 | 
				
			||||||
 | 
					           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:#a0a0a0;stroke-width:19.63722609999999900;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-6"
 | 
				
			||||||
 | 
					           style="stroke:#a0a0a0;stroke-opacity:1">
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#a0a0a0;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
 | 
				
			||||||
 | 
					             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-0"
 | 
				
			||||||
 | 
					             inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					             sodipodi:nodetypes="czcczcc" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					             id="rect4203-21-3"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="548.70886"
 | 
				
			||||||
 | 
					             y="1008.1376" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					             id="rect4203-2-6-6"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="549.79102"
 | 
				
			||||||
 | 
					             y="1042.7678" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#a0a0a0;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					             id="rect4203-2-3-8-2"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="549.79102"
 | 
				
			||||||
 | 
					             y="1076.0995" />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					             style="fill:#b3b3b3;stroke:#a0a0a0;stroke-width:19.63722609999999900;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-4"
 | 
				
			||||||
 | 
					             inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					             sodipodi:nodetypes="czc" />
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         id="path3850-1-1"
 | 
				
			||||||
 | 
					         d="m 1409.5992,670.87038 0,-128.57724 c 0,0 1.8599,-15.30681 -16.7384,-15.30681 -18.5984,0 -51.1454,0 -51.1454,0"
 | 
				
			||||||
 | 
					         style="fill:none;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         y="547.80316"
 | 
				
			||||||
 | 
					         x="1294.749"
 | 
				
			||||||
 | 
					         height="104.27072"
 | 
				
			||||||
 | 
					         width="3.2554622"
 | 
				
			||||||
 | 
					         id="rect3818-4-7"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#a0a0a0;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,944.16607,536.33294)"
 | 
				
			||||||
 | 
					         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-4"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,942.63054,386.00935)"
 | 
				
			||||||
 | 
					         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-0"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,1056.9547,536.43446)"
 | 
				
			||||||
 | 
					         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-9"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#a0a0a0;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         id="path3852-4-4"
 | 
				
			||||||
 | 
					         d="m 1369.3146,490.2451 0,70.69144 -45.5889,-32.13462 z"
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;stroke:#a0a0a0;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="1474.2273"
 | 
				
			||||||
 | 
					         y="-367.14282"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-8"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-281.45197"
 | 
				
			||||||
 | 
					         y="-1573.058"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-82"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-412.46057"
 | 
				
			||||||
 | 
					         y="-1617.4926"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:10.82955647;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-82-4"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-1617.2937"
 | 
				
			||||||
 | 
					         y="306.0546"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <g
 | 
				
			||||||
 | 
					         id="g4016"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,3107.8871,982.01044)">
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="scale(-1,-1)"
 | 
				
			||||||
 | 
					           y="-1119.1083"
 | 
				
			||||||
 | 
					           x="-1370.8767"
 | 
				
			||||||
 | 
					           height="105.80523"
 | 
				
			||||||
 | 
					           width="33.87508"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-4"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="scale(-1,-1)"
 | 
				
			||||||
 | 
					           y="-1207.0963"
 | 
				
			||||||
 | 
					           x="-1358.6217"
 | 
				
			||||||
 | 
					           height="113.43421"
 | 
				
			||||||
 | 
					           width="9.3650599"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-7-0"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           transform="matrix(1.5972925,0,0,1.509886,-99.098035,-27.987625)"
 | 
				
			||||||
 | 
					           d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
 | 
				
			||||||
 | 
					           sodipodi:ry="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:rx="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:cy="838.20984"
 | 
				
			||||||
 | 
					           sodipodi:cx="910.14746"
 | 
				
			||||||
 | 
					           id="path3226"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.14799976;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					           sodipodi:type="arc" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           transform="matrix(1.1933397,0,0,1.5429659,269.38527,-37.350485)"
 | 
				
			||||||
 | 
					           d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
 | 
				
			||||||
 | 
					           sodipodi:ry="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:rx="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:cy="838.20984"
 | 
				
			||||||
 | 
					           sodipodi:cx="910.14746"
 | 
				
			||||||
 | 
					           id="path3226-9"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;fill-opacity:1;stroke:none"
 | 
				
			||||||
 | 
					           sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:none"
 | 
				
			||||||
 | 
					         id="rect4027"
 | 
				
			||||||
 | 
					         width="73.460579"
 | 
				
			||||||
 | 
					         height="107.13"
 | 
				
			||||||
 | 
					         x="1722.2299"
 | 
				
			||||||
 | 
					         y="-207.2868"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <g
 | 
				
			||||||
 | 
					         id="g4022"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,1095.262,-713.65443)">
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="scale(-1,-1)"
 | 
				
			||||||
 | 
					           y="-1121.4039"
 | 
				
			||||||
 | 
					           x="-1505.5544"
 | 
				
			||||||
 | 
					           height="105.80523"
 | 
				
			||||||
 | 
					           width="33.87508"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:16.74562263;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="scale(-1,-1)"
 | 
				
			||||||
 | 
					           y="-1222.4006"
 | 
				
			||||||
 | 
					           x="-1494.0955"
 | 
				
			||||||
 | 
					           height="113.43421"
 | 
				
			||||||
 | 
					           width="9.3650599"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-7"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:9.11664104;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="rect3182"
 | 
				
			||||||
 | 
					           d="m 1469.5507,1220.8203 20.5952,52.7426 20.6425,-52.7426 -2.0832,-5.35 -37.0713,0 -2.0832,5.35 z"
 | 
				
			||||||
 | 
					           style="fill:#a0a0a0;fill-opacity:1;stroke:#a0a0a0;stroke-width:7.02416945;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2991-7-6-1"
 | 
				
			||||||
 | 
					         transform="translate(1482.3625,-199.43254)"
 | 
				
			||||||
 | 
					         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:#3c3c3c;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2993-4-8-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:#3c3c80;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc"
 | 
				
			||||||
 | 
					         transform="matrix(0.83611704,0,0,0.83611704,1525.1139,-151.0104)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-8-4"
 | 
				
			||||||
 | 
					         y="18.660131"
 | 
				
			||||||
 | 
					         x="1724.3145"
 | 
				
			||||||
 | 
					         height="99.221687"
 | 
				
			||||||
 | 
					         width="29.189819"
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:1.11112404000000000" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2997-9-2-0"
 | 
				
			||||||
 | 
					         y="137.45401"
 | 
				
			||||||
 | 
					         x="1724.5726"
 | 
				
			||||||
 | 
					         height="26.258072"
 | 
				
			||||||
 | 
					         width="29.724136"
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#ffffff;stroke-width:0.57680577000000000" />
 | 
				
			||||||
 | 
					      <g
 | 
				
			||||||
 | 
					         id="g4284-1-9"
 | 
				
			||||||
 | 
					         transform="translate(1061.8621,-808.41119)"
 | 
				
			||||||
 | 
					         style="stroke:#3c3c3c;stroke-opacity:1">
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           sodipodi:nodetypes="czcczcc"
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="rect4201-26-4"
 | 
				
			||||||
 | 
					           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:#3c3c3c;stroke-width:14.36538028999999900;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1108.1473"
 | 
				
			||||||
 | 
					           x="597.4068"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.265846"
 | 
				
			||||||
 | 
					           id="rect4203-0-8"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1142.7776"
 | 
				
			||||||
 | 
					           x="598.48895"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.26585"
 | 
				
			||||||
 | 
					           id="rect4203-2-4-8"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           y="1176.1093"
 | 
				
			||||||
 | 
					           x="598.48895"
 | 
				
			||||||
 | 
					           height="5.4857273"
 | 
				
			||||||
 | 
					           width="55.26585"
 | 
				
			||||||
 | 
					           id="rect4203-2-3-9-2"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           sodipodi:nodetypes="czc"
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="path4245-4-4"
 | 
				
			||||||
 | 
					           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:#3c3c3c;stroke-width:19.63722609999999900;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-6-5"
 | 
				
			||||||
 | 
					           style="stroke:#3c3c3c;stroke-opacity:1">
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#3c3c3c;stroke-width:14.36538124000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
 | 
				
			||||||
 | 
					             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-0-5"
 | 
				
			||||||
 | 
					             inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					             sodipodi:nodetypes="czcczcc" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					             id="rect4203-21-3-1"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="548.70886"
 | 
				
			||||||
 | 
					             y="1008.1376" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					             id="rect4203-2-6-6-7"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="549.79102"
 | 
				
			||||||
 | 
					             y="1042.7678" />
 | 
				
			||||||
 | 
					          <rect
 | 
				
			||||||
 | 
					             style="fill:#ffffff;stroke:#3c3c3c;stroke-width:11.82844734000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					             id="rect4203-2-3-8-2-1"
 | 
				
			||||||
 | 
					             width="55.26585"
 | 
				
			||||||
 | 
					             height="5.4857273"
 | 
				
			||||||
 | 
					             x="549.79102"
 | 
				
			||||||
 | 
					             y="1076.0995" />
 | 
				
			||||||
 | 
					          <path
 | 
				
			||||||
 | 
					             style="fill:#b3b3b3;stroke:#3c3c3c;stroke-width:19.63722609999999900;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-4-1"
 | 
				
			||||||
 | 
					             inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					             sodipodi:nodetypes="czc" />
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         id="path3850-1-1-5"
 | 
				
			||||||
 | 
					         d="m 1801.3889,678.70099 0,-128.5772 c 0,0 1.8599,-15.3068 -16.7384,-15.3068 -18.5984,0 -51.1454,0 -51.1454,0"
 | 
				
			||||||
 | 
					         style="fill:none;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         y="555.63385"
 | 
				
			||||||
 | 
					         x="1686.5388"
 | 
				
			||||||
 | 
					         height="104.27072"
 | 
				
			||||||
 | 
					         width="3.2554622"
 | 
				
			||||||
 | 
					         id="rect3818-4-7-2"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#3c3c3c;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,1335.9558,544.16359)"
 | 
				
			||||||
 | 
					         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-4-7"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,1334.4203,393.83999)"
 | 
				
			||||||
 | 
					         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-0-6"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,1448.7444,544.26509)"
 | 
				
			||||||
 | 
					         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-9-1"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#3c3c3c;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         id="path3852-4-4-4"
 | 
				
			||||||
 | 
					         d="m 1761.1043,498.07579 0,70.6914 -45.5889,-32.1346 z"
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:0.83335358000000004px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-2"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="1756.8015"
 | 
				
			||||||
 | 
					         y="-638.64288"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-8-3"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-552.95203"
 | 
				
			||||||
 | 
					         y="-1855.6323"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-82-2"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-683.96063"
 | 
				
			||||||
 | 
					         y="-1900.0669"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:10.82955647000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect3953-82-4-2"
 | 
				
			||||||
 | 
					         width="15.304287"
 | 
				
			||||||
 | 
					         height="97.947441"
 | 
				
			||||||
 | 
					         x="-1899.8679"
 | 
				
			||||||
 | 
					         y="577.55469"
 | 
				
			||||||
 | 
					         transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <g
 | 
				
			||||||
 | 
					         id="g4138">
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
 | 
				
			||||||
 | 
					           y="2055.4602"
 | 
				
			||||||
 | 
					           x="403.84506"
 | 
				
			||||||
 | 
					           height="105.80523"
 | 
				
			||||||
 | 
					           width="33.87508"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-4-6"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
 | 
				
			||||||
 | 
					           y="1967.4722"
 | 
				
			||||||
 | 
					           x="416.10007"
 | 
				
			||||||
 | 
					           height="113.43421"
 | 
				
			||||||
 | 
					           width="9.3650599"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-7-0-8"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           transform="matrix(-1.1294564,1.1294564,-1.0676506,-1.0676506,3589.5398,939.55844)"
 | 
				
			||||||
 | 
					           d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
 | 
				
			||||||
 | 
					           sodipodi:ry="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:rx="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:cy="838.20984"
 | 
				
			||||||
 | 
					           sodipodi:cx="910.14746"
 | 
				
			||||||
 | 
					           id="path3226-5"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.14799976000000030;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					           sodipodi:type="arc" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           transform="matrix(-0.84381859,0.84381859,-1.0910416,-1.0910416,3335.6033,1206.736)"
 | 
				
			||||||
 | 
					           d="m 936.41143,838.20984 c 0,14.50519 -11.75878,26.26396 -26.26397,26.26396 -14.50519,0 -26.26396,-11.75877 -26.26396,-26.26396 0,-14.50519 11.75877,-26.26397 26.26396,-26.26397 14.50519,0 26.26397,11.75878 26.26397,26.26397 z"
 | 
				
			||||||
 | 
					           sodipodi:ry="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:rx="26.263966"
 | 
				
			||||||
 | 
					           sodipodi:cy="838.20984"
 | 
				
			||||||
 | 
					           sodipodi:cx="910.14746"
 | 
				
			||||||
 | 
					           id="path3226-9-7"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;fill-opacity:1;stroke:none"
 | 
				
			||||||
 | 
					           sodipodi:type="arc" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
 | 
				
			||||||
 | 
					           y="-476.95105"
 | 
				
			||||||
 | 
					           x="2003.8865"
 | 
				
			||||||
 | 
					           height="107.13"
 | 
				
			||||||
 | 
					           width="73.460579"
 | 
				
			||||||
 | 
					           id="rect4027-6"
 | 
				
			||||||
 | 
					           style="fill:#ffffff;fill-opacity:1;stroke:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
 | 
				
			||||||
 | 
					           y="429.19318"
 | 
				
			||||||
 | 
					           x="-2057.9661"
 | 
				
			||||||
 | 
					           height="105.80523"
 | 
				
			||||||
 | 
					           width="33.87508"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-8"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:16.74562263000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <rect
 | 
				
			||||||
 | 
					           transform="matrix(-0.70710678,-0.70710678,0.70710678,-0.70710678,0,0)"
 | 
				
			||||||
 | 
					           y="328.19647"
 | 
				
			||||||
 | 
					           x="-2046.5071"
 | 
				
			||||||
 | 
					           height="113.43421"
 | 
				
			||||||
 | 
					           width="9.3650599"
 | 
				
			||||||
 | 
					           id="rect3953-82-4-1-7-9"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:9.11664103999999930;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					           inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					           id="rect3182-2"
 | 
				
			||||||
 | 
					           d="m 1662.9307,1196.5558 -22.7317,51.8577 51.8911,-22.6982 2.31,-5.2561 -26.2134,-26.2134 -5.256,2.31 z"
 | 
				
			||||||
 | 
					           style="fill:#3c3c3c;fill-opacity:1;stroke:#3c3c3c;stroke-width:7.02416944999999960;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2991-7-1-4-1"
 | 
				
			||||||
 | 
					         transform="translate(-154.10522,1432.0357)"
 | 
				
			||||||
 | 
					         d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z"
 | 
				
			||||||
 | 
					         sodipodi:ry="104.28571"
 | 
				
			||||||
 | 
					         sodipodi:rx="104.28571"
 | 
				
			||||||
 | 
					         sodipodi:cy="290.93362"
 | 
				
			||||||
 | 
					         sodipodi:cx="255.71428"
 | 
				
			||||||
 | 
					         style="fill:#bebeff;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         id="path2993-4-5-8-7"
 | 
				
			||||||
 | 
					         d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 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,-111.35384,1480.4578)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-2-8-4"
 | 
				
			||||||
 | 
					         y="1695.4933"
 | 
				
			||||||
 | 
					         x="93.81971"
 | 
				
			||||||
 | 
					         height="99.396141"
 | 
				
			||||||
 | 
					         width="20.706863"
 | 
				
			||||||
 | 
					         style="fill:#bebefa;stroke:none;fill-opacity:1" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#ffffff;stroke-width:2.92446065;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 61.259358,1683.2763 -64.6850999,-36.2114 10.7001,55.9569 53.9849999,-19.7455 z"
 | 
				
			||||||
 | 
					         id="rect4046-3-4-0"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#bebeff;stroke:#bebeff;stroke-width:1.98877633000000010;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1;stroke-opacity:1"
 | 
				
			||||||
 | 
					         d="m 50.602958,1676.4044 -43.6559999,-24.4345 6.9986999,38.1562 36.6573,-13.7217 z"
 | 
				
			||||||
 | 
					         id="rect4046-5-9"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#bebefa;stroke-width:13.63542366000000100;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         d="m 339.45286,1633.1057 c -0.2525,22.2049 -0.505,44.4098 -0.7575,66.6147 3.3299,0.032 6.6598,0.063 9.9898,0.095 -2.3515,2.3672 -4.703,4.7345 -7.0544,7.1018 31.3741,31.374 62.7482,62.7482 94.1222,94.1223 23.3413,-23.3412 46.6825,-46.6824 70.0237,-70.0236 -31.3741,-31.3899 -62.7483,-62.7798 -94.1224,-94.1697 -2.6197,2.6356 -5.2395,5.2711 -7.8593,7.9067 0.032,-3.6298 0.063,-7.2596 0.095,-10.8894 -21.4789,-0.2525 -42.9579,-0.505 -64.4368,-0.7575 z"
 | 
				
			||||||
 | 
					         id="rect3075-11-4"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         id="rect2995-0-2-8-6-8"
 | 
				
			||||||
 | 
					         y="77.228889"
 | 
				
			||||||
 | 
					         x="-1734.3357"
 | 
				
			||||||
 | 
					         height="99.396141"
 | 
				
			||||||
 | 
					         width="20.706863"
 | 
				
			||||||
 | 
					         style="fill:#bebefa;stroke:none;fill-opacity:1"
 | 
				
			||||||
 | 
					         transform="matrix(0,-1,1,0,0,0)" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.37699986;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 349.78646,1643.6829 c -0.1922,16.8985 -0.3844,33.7973 -0.5765,50.6959 2.5342,0.024 5.0684,0.047 7.6026,0.072 -1.7896,1.8014 -3.5792,3.6031 -5.3686,5.4047 23.8766,23.8766 47.7533,47.7533 71.63,71.6301 17.7636,-17.7634 35.5269,-35.5268 53.2902,-53.2902 -23.8766,-23.8888 -47.7534,-47.7775 -71.6302,-71.6662 -1.9936,2.0058 -3.9873,4.0114 -5.9811,6.0172 0.024,-2.7624 0.047,-5.5247 0.072,-8.2872 -16.3463,-0.1921 -32.6925,-0.3843 -49.0386,-0.5764 z"
 | 
				
			||||||
 | 
					         id="rect3075-11-7-8"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         sodipodi:type="arc"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#bebefa;stroke-width:6.57334423000000000;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none"
 | 
				
			||||||
 | 
					         id="path3100-2-2"
 | 
				
			||||||
 | 
					         sodipodi:cx="700"
 | 
				
			||||||
 | 
					         sodipodi:cy="812.36218"
 | 
				
			||||||
 | 
					         sodipodi:rx="10"
 | 
				
			||||||
 | 
					         sodipodi:ry="10"
 | 
				
			||||||
 | 
					         d="m 710,812.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z"
 | 
				
			||||||
 | 
					         transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         id="rect4114-4"
 | 
				
			||||||
 | 
					         width="45.086407"
 | 
				
			||||||
 | 
					         height="62.401226"
 | 
				
			||||||
 | 
					         x="-939.10236"
 | 
				
			||||||
 | 
					         y="1492.6151"
 | 
				
			||||||
 | 
					         transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         style="fill:none;stroke:#bebeff;stroke-width:25.84518814000000100;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 | 
				
			||||||
 | 
					         d="m 655.10691,1790.494 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-5"
 | 
				
			||||||
 | 
					         inkscape:connector-curvature="0"
 | 
				
			||||||
 | 
					         inkscape:transform-center-x="-9.2946303"
 | 
				
			||||||
 | 
					         sodipodi:nodetypes="csc"
 | 
				
			||||||
 | 
					         inkscape:transform-center-y="2.9369479e-005" />
 | 
				
			||||||
 | 
					      <rect
 | 
				
			||||||
 | 
					         y="1676.2623"
 | 
				
			||||||
 | 
					         x="652.97418"
 | 
				
			||||||
 | 
					         height="104.27072"
 | 
				
			||||||
 | 
					         width="3.2554622"
 | 
				
			||||||
 | 
					         id="rect3818-4-8-4-5"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
 | 
				
			||||||
 | 
					         d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
 | 
				
			||||||
 | 
					         sodipodi:ry="35.140915"
 | 
				
			||||||
 | 
					         sodipodi:rx="21.718279"
 | 
				
			||||||
 | 
					         sodipodi:cy="230.89374"
 | 
				
			||||||
 | 
					         sodipodi:cx="351.02802"
 | 
				
			||||||
 | 
					         id="path3795-4-8-7-8-1"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
 | 
				
			||||||
 | 
					         d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
 | 
				
			||||||
 | 
					         sodipodi:ry="35.140915"
 | 
				
			||||||
 | 
					         sodipodi:rx="21.718279"
 | 
				
			||||||
 | 
					         sodipodi:cy="230.89374"
 | 
				
			||||||
 | 
					         sodipodi:cx="351.02802"
 | 
				
			||||||
 | 
					         id="path3795-8-4-8-7"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
 | 
				
			||||||
 | 
					         d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z"
 | 
				
			||||||
 | 
					         sodipodi:ry="35.140915"
 | 
				
			||||||
 | 
					         sodipodi:rx="21.718279"
 | 
				
			||||||
 | 
					         sodipodi:cy="230.89374"
 | 
				
			||||||
 | 
					         sodipodi:cx="351.02802"
 | 
				
			||||||
 | 
					         id="path3795-8-4-8-2-1"
 | 
				
			||||||
 | 
					         style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
				
			||||||
 | 
					         sodipodi:type="arc" />
 | 
				
			||||||
    </g>
 | 
					    </g>
 | 
				
			||||||
  </g>
 | 
					  </g>
 | 
				
			||||||
</svg>
 | 
					</svg>
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 87 KiB  | 
@@ -1 +1 @@
 | 
				
			|||||||
sbt.version=0.12.3
 | 
					sbt.version=0.13.5
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,24 @@
 | 
				
			|||||||
import sbt._
 | 
					import sbt._
 | 
				
			||||||
import Keys._
 | 
					import Keys._
 | 
				
			||||||
import org.scalatra.sbt._
 | 
					import org.scalatra.sbt._
 | 
				
			||||||
import org.scalatra.sbt.PluginKeys._
 | 
					 | 
				
			||||||
import sbt.ScalaVersion
 | 
					 | 
				
			||||||
import twirl.sbt.TwirlPlugin._
 | 
					 | 
				
			||||||
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
 | 
					import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
 | 
				
			||||||
 | 
					import play.twirl.sbt.SbtTwirl
 | 
				
			||||||
 | 
					import play.twirl.sbt.Import.TwirlKeys._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object MyBuild extends Build {
 | 
					object MyBuild extends Build {
 | 
				
			||||||
  val Organization = "jp.sf.amateras"
 | 
					  val Organization = "jp.sf.amateras"
 | 
				
			||||||
  val Name = "gitbucket"
 | 
					  val Name = "gitbucket"
 | 
				
			||||||
  val Version = "0.0.1"
 | 
					  val Version = "0.0.1"
 | 
				
			||||||
  val ScalaVersion = "2.10.3"
 | 
					  val ScalaVersion = "2.11.2"
 | 
				
			||||||
  val ScalatraVersion = "2.2.1"
 | 
					  val ScalatraVersion = "2.3.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lazy val project = Project (
 | 
					  lazy val project = Project (
 | 
				
			||||||
    "gitbucket",
 | 
					    "gitbucket",
 | 
				
			||||||
    file("."),
 | 
					    file(".")
 | 
				
			||||||
    settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq(
 | 
					  )
 | 
				
			||||||
 | 
					  .settings(ScalatraPlugin.scalatraWithJRebel: _*)
 | 
				
			||||||
 | 
					  .settings(
 | 
				
			||||||
 | 
					    sourcesInBase := false,
 | 
				
			||||||
    organization := Organization,
 | 
					    organization := Organization,
 | 
				
			||||||
    name := Name,
 | 
					    name := Name,
 | 
				
			||||||
    version := Version,
 | 
					    version := Version,
 | 
				
			||||||
@@ -25,31 +27,34 @@ object MyBuild extends Build {
 | 
				
			|||||||
      Classpaths.typesafeReleases,
 | 
					      Classpaths.typesafeReleases,
 | 
				
			||||||
      "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
 | 
					      "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
      scalacOptions := Seq("-deprecation"),
 | 
					    scalacOptions := Seq("-deprecation", "-language:postfixOps"),
 | 
				
			||||||
    libraryDependencies ++= Seq(
 | 
					    libraryDependencies ++= Seq(
 | 
				
			||||||
        "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
 | 
					      "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
 | 
				
			||||||
 | 
					      "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
 | 
				
			||||||
      "org.scalatra" %% "scalatra" % ScalatraVersion,
 | 
					      "org.scalatra" %% "scalatra" % ScalatraVersion,
 | 
				
			||||||
      "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
 | 
					      "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
 | 
				
			||||||
      "org.scalatra" %% "scalatra-json" % ScalatraVersion,
 | 
					      "org.scalatra" %% "scalatra-json" % ScalatraVersion,
 | 
				
			||||||
        "org.json4s" %% "json4s-jackson" % "3.2.5",
 | 
					      "org.json4s" %% "json4s-jackson" % "3.2.10",
 | 
				
			||||||
        "jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
 | 
					      "jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
 | 
				
			||||||
      "commons-io" % "commons-io" % "2.4",
 | 
					      "commons-io" % "commons-io" % "2.4",
 | 
				
			||||||
      "org.pegdown" % "pegdown" % "1.4.1",
 | 
					      "org.pegdown" % "pegdown" % "1.4.1",
 | 
				
			||||||
      "org.apache.commons" % "commons-compress" % "1.5",
 | 
					      "org.apache.commons" % "commons-compress" % "1.5",
 | 
				
			||||||
      "org.apache.commons" % "commons-email" % "1.3.1",
 | 
					      "org.apache.commons" % "commons-email" % "1.3.1",
 | 
				
			||||||
      "org.apache.httpcomponents" % "httpclient" % "4.3",
 | 
					      "org.apache.httpcomponents" % "httpclient" % "4.3",
 | 
				
			||||||
        "com.typesafe.slick" %% "slick" % "1.0.1",
 | 
					      "org.apache.sshd" % "apache-sshd" % "0.11.0",
 | 
				
			||||||
 | 
					      "com.typesafe.slick" %% "slick" % "2.1.0-RC3",
 | 
				
			||||||
      "com.novell.ldap" % "jldap" % "2009-10-07",
 | 
					      "com.novell.ldap" % "jldap" % "2009-10-07",
 | 
				
			||||||
        "com.h2database" % "h2" % "1.3.173",
 | 
					      "org.quartz-scheduler" % "quartz" % "2.2.1",
 | 
				
			||||||
 | 
					      "com.h2database" % "h2" % "1.4.180",
 | 
				
			||||||
      "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
 | 
					      "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
 | 
				
			||||||
      "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
 | 
					      "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
 | 
				
			||||||
        "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
 | 
					      "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
 | 
				
			||||||
        "junit" % "junit" % "4.11" % "test"
 | 
					      "junit" % "junit" % "4.11" % "test",
 | 
				
			||||||
 | 
					      "com.typesafe.play" %% "twirl-compiler" % "1.0.2"
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    EclipseKeys.withSource := true,
 | 
					    EclipseKeys.withSource := true,
 | 
				
			||||||
    javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
 | 
					    javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
 | 
				
			||||||
    testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
 | 
					    testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
 | 
				
			||||||
    packageOptions += Package.MainClass("JettyLauncher")
 | 
					    packageOptions += Package.MainClass("JettyLauncher")
 | 
				
			||||||
    ) ++ seq(Twirl.settings: _*)
 | 
					  ).enablePlugins(SbtTwirl)
 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
 | 
					addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
 | 
					addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
 | 
					addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
 | 
					addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
 | 
					addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								sbt-launch-0.13.5.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								sbt-launch-0.13.5.jar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								sbt.bat
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								sbt.bat
									
									
									
									
									
								
							@@ -1,2 +1,2 @@
 | 
				
			|||||||
set SCRIPT_DIR=%~dp0
 | 
					set SCRIPT_DIR=%~dp0
 | 
				
			||||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.12.3.jar" %*
 | 
					java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.5.jar" %*
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								sbt.sh
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								sbt.sh
									
									
									
									
									
								
							@@ -1 +1 @@
 | 
				
			|||||||
java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
 | 
					java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.5.jar "$@"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,8 @@
 | 
				
			|||||||
import org.eclipse.jetty.io.EndPoint;
 | 
					 | 
				
			||||||
import org.eclipse.jetty.server.Request;
 | 
					 | 
				
			||||||
import org.eclipse.jetty.server.Server;
 | 
					import org.eclipse.jetty.server.Server;
 | 
				
			||||||
import org.eclipse.jetty.server.nio.SelectChannelConnector;
 | 
					import org.eclipse.jetty.server.nio.SelectChannelConnector;
 | 
				
			||||||
import org.eclipse.jetty.webapp.WebAppContext;
 | 
					import org.eclipse.jetty.webapp.WebAppContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.File;
 | 
				
			||||||
import java.net.URL;
 | 
					import java.net.URL;
 | 
				
			||||||
import java.security.ProtectionDomain;
 | 
					import java.security.ProtectionDomain;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,8 +23,8 @@ public class JettyLauncher {
 | 
				
			|||||||
                        port = Integer.parseInt(dim[1]);
 | 
					                        port = Integer.parseInt(dim[1]);
 | 
				
			||||||
                    } else if(dim[0].equals("--prefix")) {
 | 
					                    } else if(dim[0].equals("--prefix")) {
 | 
				
			||||||
                        contextPath = dim[1];
 | 
					                        contextPath = dim[1];
 | 
				
			||||||
                    } else if(dim[0].equals("--https") && (dim[1].equals("1") || dim[1].equals("true"))) {
 | 
					                    } else if(dim[0].equals("--gitbucket.home")){
 | 
				
			||||||
                        forceHttps = true;
 | 
					                        System.setProperty("gitbucket.home", dim[1]);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -34,7 +32,7 @@ public class JettyLauncher {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Server server = new Server();
 | 
					        Server server = new Server();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        HttpsSupportConnector connector = new HttpsSupportConnector(forceHttps);
 | 
					        SelectChannelConnector connector = new SelectChannelConnector();
 | 
				
			||||||
        if(host != null) {
 | 
					        if(host != null) {
 | 
				
			||||||
            connector.setHost(host);
 | 
					            connector.setHost(host);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -44,6 +42,14 @@ public class JettyLauncher {
 | 
				
			|||||||
        server.addConnector(connector);
 | 
					        server.addConnector(connector);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        WebAppContext context = new WebAppContext();
 | 
					        WebAppContext context = new WebAppContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        File tmpDir = new File(getGitBucketHome(), "tmp");
 | 
				
			||||||
 | 
					        if(tmpDir.exists()){
 | 
				
			||||||
 | 
					            deleteDirectory(tmpDir);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        tmpDir.mkdirs();
 | 
				
			||||||
 | 
					        context.setTempDirectory(tmpDir);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
 | 
					        ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
 | 
				
			||||||
        URL location = domain.getCodeSource().getLocation();
 | 
					        URL location = domain.getCodeSource().getLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,25 +57,35 @@ public class JettyLauncher {
 | 
				
			|||||||
        context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
 | 
					        context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
 | 
				
			||||||
        context.setServer(server);
 | 
					        context.setServer(server);
 | 
				
			||||||
        context.setWar(location.toExternalForm());
 | 
					        context.setWar(location.toExternalForm());
 | 
				
			||||||
 | 
					        if (forceHttps) {
 | 
				
			||||||
 | 
					            context.setInitParameter("org.scalatra.ForceHttps", "true");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        server.setHandler(context);
 | 
					        server.setHandler(context);
 | 
				
			||||||
        server.start();
 | 
					        server.start();
 | 
				
			||||||
        server.join();
 | 
					        server.join();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HttpsSupportConnector extends SelectChannelConnector {
 | 
					    private static File getGitBucketHome(){
 | 
				
			||||||
    private boolean forceHttps;
 | 
					        String home = System.getProperty("gitbucket.home");
 | 
				
			||||||
 | 
					        if(home != null && home.length() > 0){
 | 
				
			||||||
    public HttpsSupportConnector(boolean forceHttps) {
 | 
					            return new File(home);
 | 
				
			||||||
        this.forceHttps = forceHttps;
 | 
					        }
 | 
				
			||||||
 | 
					        home = System.getenv("GITBUCKET_HOME");
 | 
				
			||||||
 | 
					        if(home != null && home.length() > 0){
 | 
				
			||||||
 | 
					            return new File(home);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return new File(System.getProperty("user.home"), ".gitbucket");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    private static void deleteDirectory(File dir){
 | 
				
			||||||
    public void customize(final EndPoint endpoint, final Request request) throws IOException {
 | 
					        for(File file: dir.listFiles()){
 | 
				
			||||||
        if (this.forceHttps) {
 | 
					            if(file.isFile()){
 | 
				
			||||||
            request.setScheme("https");
 | 
					                file.delete();
 | 
				
			||||||
            super.customize(endpoint, request);
 | 
					            } else if(file.isDirectory()){
 | 
				
			||||||
 | 
					                deleteDirectory(file);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        dir.delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										93
									
								
								src/main/java/util/PatchUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/main/java/util/PatchUtil.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					package util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.errors.PatchApplyException;
 | 
				
			||||||
 | 
					import org.eclipse.jgit.diff.RawText;
 | 
				
			||||||
 | 
					import org.eclipse.jgit.internal.JGitText;
 | 
				
			||||||
 | 
					import org.eclipse.jgit.patch.FileHeader;
 | 
				
			||||||
 | 
					import org.eclipse.jgit.patch.HunkHeader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.ByteArrayOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.text.MessageFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class PatchUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static String apply(String source, String patch, FileHeader fh)
 | 
				
			||||||
 | 
					            throws IOException, PatchApplyException {
 | 
				
			||||||
 | 
					        RawText rt = new RawText(source.getBytes("UTF-8"));
 | 
				
			||||||
 | 
					        List<String> oldLines = new ArrayList<String>(rt.size());
 | 
				
			||||||
 | 
					        for (int i = 0; i < rt.size(); i++)
 | 
				
			||||||
 | 
					            oldLines.add(rt.getString(i));
 | 
				
			||||||
 | 
					        List<String> newLines = new ArrayList<String>(oldLines);
 | 
				
			||||||
 | 
					        for (HunkHeader hh : fh.getHunks()) {
 | 
				
			||||||
 | 
					            ByteArrayOutputStream out = new ByteArrayOutputStream();
 | 
				
			||||||
 | 
					            out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset());
 | 
				
			||||||
 | 
					            RawText hrt = new RawText(out.toByteArray());
 | 
				
			||||||
 | 
					            List<String> hunkLines = new ArrayList<String>(hrt.size());
 | 
				
			||||||
 | 
					            for (int i = 0; i < hrt.size(); i++)
 | 
				
			||||||
 | 
					                hunkLines.add(hrt.getString(i));
 | 
				
			||||||
 | 
					            int pos = 0;
 | 
				
			||||||
 | 
					            for (int j = 1; j < hunkLines.size(); j++) {
 | 
				
			||||||
 | 
					                String hunkLine = hunkLines.get(j);
 | 
				
			||||||
 | 
					                switch (hunkLine.charAt(0)) {
 | 
				
			||||||
 | 
					                    case ' ':
 | 
				
			||||||
 | 
					                        if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
 | 
				
			||||||
 | 
					                                hunkLine.substring(1))) {
 | 
				
			||||||
 | 
					                            throw new PatchApplyException(MessageFormat.format(
 | 
				
			||||||
 | 
					                                    JGitText.get().patchApplyException, hh));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        pos++;
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    case '-':
 | 
				
			||||||
 | 
					                        if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals(
 | 
				
			||||||
 | 
					                                hunkLine.substring(1))) {
 | 
				
			||||||
 | 
					                            throw new PatchApplyException(MessageFormat.format(
 | 
				
			||||||
 | 
					                                    JGitText.get().patchApplyException, hh));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        newLines.remove(hh.getNewStartLine() - 1 + pos);
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    case '+':
 | 
				
			||||||
 | 
					                        newLines.add(hh.getNewStartLine() - 1 + pos,
 | 
				
			||||||
 | 
					                                hunkLine.substring(1));
 | 
				
			||||||
 | 
					                        pos++;
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!isNoNewlineAtEndOfFile(fh))
 | 
				
			||||||
 | 
					            newLines.add(""); //$NON-NLS-1$
 | 
				
			||||||
 | 
					        if (!rt.isMissingNewlineAtEnd())
 | 
				
			||||||
 | 
					            oldLines.add(""); //$NON-NLS-1$
 | 
				
			||||||
 | 
					        if (!isChanged(oldLines, newLines))
 | 
				
			||||||
 | 
					            return null; // don't touch the file
 | 
				
			||||||
 | 
					        StringBuilder sb = new StringBuilder();
 | 
				
			||||||
 | 
					        for (String l : newLines) {
 | 
				
			||||||
 | 
					            // don't bother handling line endings - if it was windows, the \r is
 | 
				
			||||||
 | 
					            // still there!
 | 
				
			||||||
 | 
					            sb.append(l).append('\n');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        sb.deleteCharAt(sb.length() - 1);
 | 
				
			||||||
 | 
					        return sb.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static boolean isChanged(List<String> ol, List<String> nl) {
 | 
				
			||||||
 | 
					        if (ol.size() != nl.size())
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        for (int i = 0; i < ol.size(); i++)
 | 
				
			||||||
 | 
					            if (!ol.get(i).equals(nl.get(i)))
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static boolean isNoNewlineAtEndOfFile(FileHeader fh) {
 | 
				
			||||||
 | 
					        HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1);
 | 
				
			||||||
 | 
					        RawText lhrt = new RawText(lastHunk.getBuffer());
 | 
				
			||||||
 | 
					        return lhrt.getString(lhrt.size() - 1).equals(
 | 
				
			||||||
 | 
					                "\\ No newline at end of file"); //$NON-NLS-1$
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/main/resources/update/1_12.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/main/resources/update/1_12.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE SSH_KEY (
 | 
				
			||||||
 | 
					  USER_NAME VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					  SSH_KEY_ID INT AUTO_INCREMENT,
 | 
				
			||||||
 | 
					  TITLE VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					  PUBLIC_KEY TEXT NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
 | 
				
			||||||
 | 
					ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/main/resources/update/1_13.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/main/resources/update/1_13.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					DROP TABLE COMMIT_LOG;
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/main/resources/update/1_8.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/main/resources/update/1_8.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/main/resources/update/2_3.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/main/resources/update/2_3.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					CREATE TABLE PLUGIN (
 | 
				
			||||||
 | 
					  PLUGIN_ID VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					  VERSION   VARCHAR(100) NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);
 | 
				
			||||||
@@ -1,17 +1,27 @@
 | 
				
			|||||||
 | 
					import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
 | 
				
			||||||
import app._
 | 
					import app._
 | 
				
			||||||
 | 
					//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
 | 
				
			||||||
import org.scalatra._
 | 
					import org.scalatra._
 | 
				
			||||||
import javax.servlet._
 | 
					import javax.servlet._
 | 
				
			||||||
 | 
					import java.util.EnumSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ScalatraBootstrap extends LifeCycle {
 | 
					class ScalatraBootstrap extends LifeCycle {
 | 
				
			||||||
  override def init(context: ServletContext) {
 | 
					  override def init(context: ServletContext) {
 | 
				
			||||||
 | 
					    // Register TransactionFilter and BasicAuthenticationFilter at first
 | 
				
			||||||
 | 
					    context.addFilter("transactionFilter", new TransactionFilter)
 | 
				
			||||||
 | 
					    context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
 | 
				
			||||||
 | 
					    context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
 | 
				
			||||||
 | 
					    context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
 | 
				
			||||||
 | 
					    context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
 | 
				
			||||||
 | 
					    context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Register controllers
 | 
				
			||||||
    context.mount(new IndexController, "/")
 | 
					    context.mount(new IndexController, "/")
 | 
				
			||||||
    context.mount(new SearchController, "/")
 | 
					    context.mount(new SearchController, "/")
 | 
				
			||||||
    context.mount(new FileUploadController, "/upload")
 | 
					    context.mount(new FileUploadController, "/upload")
 | 
				
			||||||
    context.mount(new SignInController, "/*")
 | 
					 | 
				
			||||||
    context.mount(new DashboardController, "/*")
 | 
					    context.mount(new DashboardController, "/*")
 | 
				
			||||||
    context.mount(new UserManagementController, "/*")
 | 
					    context.mount(new UserManagementController, "/*")
 | 
				
			||||||
    context.mount(new SystemSettingsController, "/*")
 | 
					    context.mount(new SystemSettingsController, "/*")
 | 
				
			||||||
    context.mount(new CreateRepositoryController, "/*")
 | 
					 | 
				
			||||||
    context.mount(new AccountController, "/*")
 | 
					    context.mount(new AccountController, "/*")
 | 
				
			||||||
    context.mount(new RepositoryViewerController, "/*")
 | 
					    context.mount(new RepositoryViewerController, "/*")
 | 
				
			||||||
    context.mount(new WikiController, "/*")
 | 
					    context.mount(new WikiController, "/*")
 | 
				
			||||||
@@ -21,6 +31,7 @@ class ScalatraBootstrap extends LifeCycle {
 | 
				
			|||||||
    context.mount(new PullRequestsController, "/*")
 | 
					    context.mount(new PullRequestsController, "/*")
 | 
				
			||||||
    context.mount(new RepositorySettingsController, "/*")
 | 
					    context.mount(new RepositorySettingsController, "/*")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create GITBUCKET_HOME directory if it does not exist
 | 
				
			||||||
    val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
 | 
					    val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
 | 
				
			||||||
    if(!dir.exists){
 | 
					    if(!dir.exists){
 | 
				
			||||||
      dir.mkdirs()
 | 
					      dir.mkdirs()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,27 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import util.{FileUtil, OneselfAuthenticator}
 | 
					import util._
 | 
				
			||||||
import util.StringUtil._
 | 
					import util.StringUtil._
 | 
				
			||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
 | 
					import ssh.SshUtil
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.scalatra.FlashMapSupport
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
 | 
					import org.eclipse.jgit.lib.{FileMode, Constants}
 | 
				
			||||||
 | 
					import org.eclipse.jgit.dircache.DirCache
 | 
				
			||||||
 | 
					import model.GroupMember
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountController extends AccountControllerBase
 | 
					class AccountController extends AccountControllerBase
 | 
				
			||||||
  with SystemSettingsService with AccountService with RepositoryService with ActivityService
 | 
					  with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
 | 
				
			||||||
  with OneselfAuthenticator
 | 
					  with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait AccountControllerBase extends AccountManagementControllerBase with FlashMapSupport {
 | 
					trait AccountControllerBase extends AccountManagementControllerBase {
 | 
				
			||||||
  self: SystemSettingsService with AccountService with RepositoryService with ActivityService
 | 
					  self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
 | 
				
			||||||
    with OneselfAuthenticator =>
 | 
					    with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
 | 
					  case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
 | 
				
			||||||
                            url: Option[String], fileId: Option[String])
 | 
					                            url: Option[String], fileId: Option[String])
 | 
				
			||||||
@@ -21,6 +29,8 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
 | 
				
			|||||||
  case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
 | 
					  case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
 | 
				
			||||||
                             url: Option[String], fileId: Option[String], clearImage: Boolean)
 | 
					                             url: Option[String], fileId: Option[String], clearImage: Boolean)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class SshKeyForm(title: String, publicKey: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val newForm = mapping(
 | 
					  val newForm = mapping(
 | 
				
			||||||
    "userName"    -> trim(label("User name"    , text(required, maxlength(100), identifier, uniqueUserName))),
 | 
					    "userName"    -> trim(label("User name"    , text(required, maxlength(100), identifier, uniqueUserName))),
 | 
				
			||||||
    "password"    -> trim(label("Password"     , text(required, maxlength(20)))),
 | 
					    "password"    -> trim(label("Password"     , text(required, maxlength(20)))),
 | 
				
			||||||
@@ -39,6 +49,45 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
 | 
				
			|||||||
    "clearImage"  -> trim(label("Clear image"  , boolean()))
 | 
					    "clearImage"  -> trim(label("Clear image"  , boolean()))
 | 
				
			||||||
  )(AccountEditForm.apply)
 | 
					  )(AccountEditForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val sshKeyForm = mapping(
 | 
				
			||||||
 | 
					    "title"     -> trim(label("Title", text(required, maxlength(100)))),
 | 
				
			||||||
 | 
					    "publicKey" -> trim(label("Key"  , text(required, validPublicKey)))
 | 
				
			||||||
 | 
					  )(SshKeyForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
 | 
				
			||||||
 | 
					  case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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()))),
 | 
				
			||||||
 | 
					    "members"   -> trim(label("Members"    ,text(required, members)))
 | 
				
			||||||
 | 
					  )(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()))),
 | 
				
			||||||
 | 
					    "members"    -> trim(label("Members"     ,text(required, members))),
 | 
				
			||||||
 | 
					    "clearImage" -> trim(label("Clear image" ,boolean()))
 | 
				
			||||||
 | 
					  )(EditGroupForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
 | 
				
			||||||
 | 
					  case class ForkRepositoryForm(owner: String, name: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val newRepositoryForm = mapping(
 | 
				
			||||||
 | 
					    "owner"        -> trim(label("Owner"          , text(required, maxlength(40), identifier, existsAccount))),
 | 
				
			||||||
 | 
					    "name"         -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
 | 
				
			||||||
 | 
					    "description"  -> trim(label("Description"    , optional(text()))),
 | 
				
			||||||
 | 
					    "isPrivate"    -> trim(label("Repository Type", boolean())),
 | 
				
			||||||
 | 
					    "createReadme" -> trim(label("Create README"  , boolean()))
 | 
				
			||||||
 | 
					  )(RepositoryCreationForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val forkRepositoryForm = mapping(
 | 
				
			||||||
 | 
					    "owner" -> trim(label("Repository owner", text(required))),
 | 
				
			||||||
 | 
					    "name"  -> trim(label("Repository name",  text(required)))
 | 
				
			||||||
 | 
					  )(ForkRepositoryForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Displays user information.
 | 
					   * Displays user information.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -53,18 +102,30 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
 | 
				
			|||||||
            getActivitiesByUser(userName, true))
 | 
					            getActivitiesByUser(userName, true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Members
 | 
					        // Members
 | 
				
			||||||
        case "members" if(account.isGroupAccount) =>
 | 
					        case "members" if(account.isGroupAccount) => {
 | 
				
			||||||
          _root_.account.html.members(account, getGroupMembers(account.userName))
 | 
					          val members = getGroupMembers(account.userName)
 | 
				
			||||||
 | 
					          _root_.account.html.members(account, members.map(_.userName),
 | 
				
			||||||
 | 
					            context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Repositories
 | 
					        // Repositories
 | 
				
			||||||
        case _ =>
 | 
					        case _ => {
 | 
				
			||||||
 | 
					          val members = getGroupMembers(account.userName)
 | 
				
			||||||
          _root_.account.html.repositories(account,
 | 
					          _root_.account.html.repositories(account,
 | 
				
			||||||
            if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
 | 
					            if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
 | 
				
			||||||
            getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
 | 
					            getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
 | 
				
			||||||
 | 
					            context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } getOrElse NotFound
 | 
					    } getOrElse NotFound
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:userName.atom") {
 | 
				
			||||||
 | 
					    val userName = params("userName")
 | 
				
			||||||
 | 
					    contentType = "application/atom+xml; type=feed"
 | 
				
			||||||
 | 
					    helper.xml.feed(getActivitiesByUser(userName, true))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/:userName/_avatar"){
 | 
					  get("/:userName/_avatar"){
 | 
				
			||||||
    val userName = params("userName")
 | 
					    val userName = params("userName")
 | 
				
			||||||
    getAccountByUserName(userName).flatMap(_.image).map { image =>
 | 
					    getAccountByUserName(userName).flatMap(_.image).map { image =>
 | 
				
			||||||
@@ -78,7 +139,9 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  get("/:userName/_edit")(oneselfOnly {
 | 
					  get("/:userName/_edit")(oneselfOnly {
 | 
				
			||||||
    val userName = params("userName")
 | 
					    val userName = params("userName")
 | 
				
			||||||
    getAccountByUserName(userName).map(x => account.html.edit(Some(x), flash.get("info"))) getOrElse NotFound
 | 
					    getAccountByUserName(userName).map { x =>
 | 
				
			||||||
 | 
					      account.html.edit(x, flash.get("info"))
 | 
				
			||||||
 | 
					    } getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/:userName/_edit", editForm)(oneselfOnly { form =>
 | 
					  post("/:userName/_edit", editForm)(oneselfOnly { form =>
 | 
				
			||||||
@@ -97,22 +160,275 @@ trait AccountControllerBase extends AccountManagementControllerBase with FlashMa
 | 
				
			|||||||
    } getOrElse NotFound
 | 
					    } getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:userName/_delete")(oneselfOnly {
 | 
				
			||||||
 | 
					    val userName = params("userName")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getAccountByUserName(userName, true).foreach { account =>
 | 
				
			||||||
 | 
					      // Remove repositories
 | 
				
			||||||
 | 
					      getRepositoryNamesOfUser(userName).foreach { repositoryName =>
 | 
				
			||||||
 | 
					        deleteRepository(userName, repositoryName)
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
 | 
				
			||||||
 | 
					      removeUserRelatedData(userName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      updateAccount(account.copy(isRemoved = true))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    session.invalidate
 | 
				
			||||||
 | 
					    redirect("/")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:userName/_ssh")(oneselfOnly {
 | 
				
			||||||
 | 
					    val userName = params("userName")
 | 
				
			||||||
 | 
					    getAccountByUserName(userName).map { x =>
 | 
				
			||||||
 | 
					      account.html.ssh(x, getPublicKeys(x.userName))
 | 
				
			||||||
 | 
					    } getOrElse NotFound
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
 | 
				
			||||||
 | 
					    val userName = params("userName")
 | 
				
			||||||
 | 
					    addPublicKey(userName, form.title, form.publicKey)
 | 
				
			||||||
 | 
					    redirect(s"/${userName}/_ssh")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:userName/_ssh/delete/:id")(oneselfOnly {
 | 
				
			||||||
 | 
					    val userName = params("userName")
 | 
				
			||||||
 | 
					    val sshKeyId = params("id").toInt
 | 
				
			||||||
 | 
					    deletePublicKey(userName, sshKeyId)
 | 
				
			||||||
 | 
					    redirect(s"/${userName}/_ssh")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/register"){
 | 
					  get("/register"){
 | 
				
			||||||
    if(loadSystemSettings().allowAccountRegistration){
 | 
					    if(context.settings.allowAccountRegistration){
 | 
				
			||||||
      if(context.loginAccount.isDefined){
 | 
					      if(context.loginAccount.isDefined){
 | 
				
			||||||
        redirect("/")
 | 
					        redirect("/")
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        account.html.edit(None, None)
 | 
					        account.html.register()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else NotFound
 | 
					    } else NotFound
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/register", newForm){ form =>
 | 
					  post("/register", newForm){ form =>
 | 
				
			||||||
    if(loadSystemSettings().allowAccountRegistration){
 | 
					    if(context.settings.allowAccountRegistration){
 | 
				
			||||||
      createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
 | 
					      createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
 | 
				
			||||||
      updateImage(form.userName, form.fileId, false)
 | 
					      updateImage(form.userName, form.fileId, false)
 | 
				
			||||||
      redirect("/signin")
 | 
					      redirect("/signin")
 | 
				
			||||||
    } else NotFound
 | 
					    } else NotFound
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/groups/new")(usersOnly {
 | 
				
			||||||
 | 
					    account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/groups/new", newGroupForm)(usersOnly { form =>
 | 
				
			||||||
 | 
					    createGroup(form.groupName, form.url)
 | 
				
			||||||
 | 
					    updateGroupMembers(form.groupName, form.members.split(",").map {
 | 
				
			||||||
 | 
					      _.split(":") match {
 | 
				
			||||||
 | 
					        case Array(userName, isManager) => (userName, isManager.toBoolean)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }.toList)
 | 
				
			||||||
 | 
					    updateImage(form.groupName, form.fileId, false)
 | 
				
			||||||
 | 
					    redirect(s"/${form.groupName}")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:groupName/_editgroup")(managersOnly {
 | 
				
			||||||
 | 
					    defining(params("groupName")){ groupName =>
 | 
				
			||||||
 | 
					      account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:groupName/_deletegroup")(managersOnly {
 | 
				
			||||||
 | 
					    defining(params("groupName")){ groupName =>
 | 
				
			||||||
 | 
					      // Remove from GROUP_MEMBER
 | 
				
			||||||
 | 
					      updateGroupMembers(groupName, Nil)
 | 
				
			||||||
 | 
					      // Remove repositories
 | 
				
			||||||
 | 
					      getRepositoryNamesOfUser(groupName).foreach { repositoryName =>
 | 
				
			||||||
 | 
					        deleteRepository(groupName, repositoryName)
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    redirect("/")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/:groupName/_editgroup", editGroupForm)(managersOnly { form =>
 | 
				
			||||||
 | 
					    defining(params("groupName"), form.members.split(",").map {
 | 
				
			||||||
 | 
					      _.split(":") match {
 | 
				
			||||||
 | 
					        case Array(userName, isManager) => (userName, isManager.toBoolean)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }.toList){ case (groupName, members) =>
 | 
				
			||||||
 | 
					      getAccountByUserName(groupName, true).map { account =>
 | 
				
			||||||
 | 
					        updateGroup(groupName, form.url, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update GROUP_MEMBER
 | 
				
			||||||
 | 
					        updateGroupMembers(form.groupName, members)
 | 
				
			||||||
 | 
					        // Update COLLABORATOR for group repositories
 | 
				
			||||||
 | 
					        getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
 | 
				
			||||||
 | 
					          removeCollaborators(form.groupName, repositoryName)
 | 
				
			||||||
 | 
					          members.foreach { case (userName, isManager) =>
 | 
				
			||||||
 | 
					            addCollaborator(form.groupName, repositoryName, userName)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updateImage(form.groupName, form.fileId, form.clearImage)
 | 
				
			||||||
 | 
					        redirect(s"/${form.groupName}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      } getOrElse NotFound
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Show the new repository form.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get("/new")(usersOnly {
 | 
				
			||||||
 | 
					    account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Create new repository.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  post("/new", newRepositoryForm)(usersOnly { form =>
 | 
				
			||||||
 | 
					    LockUtil.lock(s"${form.owner}/${form.name}"){
 | 
				
			||||||
 | 
					      if(getRepository(form.owner, form.name, context.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, form.owner, form.description, form.isPrivate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add collaborators for group repository
 | 
				
			||||||
 | 
					        if(ownerAccount.isGroupAccount){
 | 
				
			||||||
 | 
					          getGroupMembers(form.owner).foreach { member =>
 | 
				
			||||||
 | 
					            addCollaborator(form.owner, form.name, member.userName)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Insert default labels
 | 
				
			||||||
 | 
					        insertDefaultLabels(form.owner, form.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Create the actual repository
 | 
				
			||||||
 | 
					        val gitdir = getRepositoryDir(form.owner, form.name)
 | 
				
			||||||
 | 
					        JGitUtil.initRepository(gitdir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(form.createReadme){
 | 
				
			||||||
 | 
					          using(Git.open(gitdir)){ git =>
 | 
				
			||||||
 | 
					            val builder  = DirCache.newInCore.builder()
 | 
				
			||||||
 | 
					            val inserter = git.getRepository.newObjectInserter()
 | 
				
			||||||
 | 
					            val headId   = git.getRepository.resolve(Constants.HEAD + "^{commit}")
 | 
				
			||||||
 | 
					            val content  = if(form.description.nonEmpty){
 | 
				
			||||||
 | 
					              form.name + "\n" +
 | 
				
			||||||
 | 
					              "===============\n" +
 | 
				
			||||||
 | 
					              "\n" +
 | 
				
			||||||
 | 
					              form.description.get
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              form.name + "\n" +
 | 
				
			||||||
 | 
					              "===============\n"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
 | 
				
			||||||
 | 
					              inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
 | 
				
			||||||
 | 
					            builder.finish()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
 | 
				
			||||||
 | 
					              Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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}")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/fork")(readableUsersOnly { repository =>
 | 
				
			||||||
 | 
					    val loginAccount   = context.loginAccount.get
 | 
				
			||||||
 | 
					    val loginUserName  = loginAccount.userName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LockUtil.lock(s"${loginUserName}/${repository.name}"){
 | 
				
			||||||
 | 
					      if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
 | 
				
			||||||
 | 
					        // redirect to the repository if repository already exists
 | 
				
			||||||
 | 
					        redirect(s"/${loginUserName}/${repository.name}")
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Record activity
 | 
				
			||||||
 | 
					        recordForkActivity(repository.owner, repository.name, loginUserName)
 | 
				
			||||||
 | 
					        // redirect to the repository
 | 
				
			||||||
 | 
					        redirect(s"/${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, messages: Messages): Option[String] =
 | 
				
			||||||
 | 
					      if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def uniqueRepository: Constraint = new Constraint(){
 | 
				
			||||||
 | 
					    override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
 | 
				
			||||||
 | 
					      params.get("owner").flatMap { userName =>
 | 
				
			||||||
 | 
					        getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def members: Constraint = new Constraint(){
 | 
				
			||||||
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] = {
 | 
				
			||||||
 | 
					      if(value.split(",").exists {
 | 
				
			||||||
 | 
					        _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
 | 
				
			||||||
 | 
					      }) None else Some("Must select one manager at least.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def validPublicKey: Constraint = new Constraint(){
 | 
				
			||||||
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
 | 
				
			||||||
 | 
					     case Some(_) => None
 | 
				
			||||||
 | 
					     case None => Some("Key is invalid.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,31 +3,32 @@ package app
 | 
				
			|||||||
import _root_.util.Directory._
 | 
					import _root_.util.Directory._
 | 
				
			||||||
import _root_.util.Implicits._
 | 
					import _root_.util.Implicits._
 | 
				
			||||||
import _root_.util.ControlUtil._
 | 
					import _root_.util.ControlUtil._
 | 
				
			||||||
import _root_.util.{FileUtil, Validations, Keys}
 | 
					import _root_.util.{StringUtil, FileUtil, Validations, Keys}
 | 
				
			||||||
import org.scalatra._
 | 
					import org.scalatra._
 | 
				
			||||||
import org.scalatra.json._
 | 
					import org.scalatra.json._
 | 
				
			||||||
import org.json4s._
 | 
					import org.json4s._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
import model.Account
 | 
					import model._
 | 
				
			||||||
import scala.Some
 | 
					import service.{SystemSettingsService, AccountService}
 | 
				
			||||||
import service.AccountService
 | 
					import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 | 
				
			||||||
import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
 | 
					 | 
				
			||||||
import java.text.SimpleDateFormat
 | 
					 | 
				
			||||||
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
 | 
					import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
 | 
				
			||||||
 | 
					import org.scalatra.i18n._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides generic features for controller implementations.
 | 
					 * Provides generic features for controller implementations.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
abstract class ControllerBase extends ScalatraFilter
 | 
					abstract class ControllerBase extends ScalatraFilter
 | 
				
			||||||
  with ClientSideValidationFormSupport with JacksonJsonSupport with Validations {
 | 
					  with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
 | 
				
			||||||
 | 
					  with SystemSettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  implicit val jsonFormats = DefaultFormats
 | 
					  implicit val jsonFormats = DefaultFormats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Don't set content type via Accept header.
 | 
					// TODO Scala 2.11
 | 
				
			||||||
  override def format(implicit request: HttpServletRequest) = ""
 | 
					//  // Don't set content type via Accept header.
 | 
				
			||||||
 | 
					//  override def format(implicit request: HttpServletRequest) = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
 | 
					  override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try {
 | 
				
			||||||
    val httpRequest  = request.asInstanceOf[HttpServletRequest]
 | 
					    val httpRequest  = request.asInstanceOf[HttpServletRequest]
 | 
				
			||||||
    val httpResponse = response.asInstanceOf[HttpServletResponse]
 | 
					    val httpResponse = response.asInstanceOf[HttpServletResponse]
 | 
				
			||||||
    val context      = request.getServletContext.getContextPath
 | 
					    val context      = request.getServletContext.getContextPath
 | 
				
			||||||
@@ -35,15 +36,16 @@ abstract class ControllerBase extends ScalatraFilter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if(path.startsWith("/console/")){
 | 
					    if(path.startsWith("/console/")){
 | 
				
			||||||
      val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
 | 
					      val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
 | 
				
			||||||
 | 
					      val baseUrl = this.baseUrl(httpRequest)
 | 
				
			||||||
      if(account == null){
 | 
					      if(account == null){
 | 
				
			||||||
        // Redirect to login form
 | 
					        // Redirect to login form
 | 
				
			||||||
        httpResponse.sendRedirect(context + "/signin?" + path)
 | 
					        httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path))
 | 
				
			||||||
      } else if(account.isAdmin){
 | 
					      } else if(account.isAdmin){
 | 
				
			||||||
        // H2 Console (administrators only)
 | 
					        // H2 Console (administrators only)
 | 
				
			||||||
        chain.doFilter(request, response)
 | 
					        chain.doFilter(request, response)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        // Redirect to dashboard
 | 
					        // Redirect to dashboard
 | 
				
			||||||
        httpResponse.sendRedirect(context + "/")
 | 
					        httpResponse.sendRedirect(baseUrl + "/")
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if(path.startsWith("/git/")){
 | 
					    } else if(path.startsWith("/git/")){
 | 
				
			||||||
      // Git repository
 | 
					      // Git repository
 | 
				
			||||||
@@ -52,15 +54,24 @@ abstract class ControllerBase extends ScalatraFilter
 | 
				
			|||||||
      // Scalatra actions
 | 
					      // Scalatra actions
 | 
				
			||||||
      super.doFilter(request, response, chain)
 | 
					      super.doFilter(request, response, chain)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    contextCache.remove();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val contextCache = new java.lang.ThreadLocal[Context]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the context object for the request.
 | 
					   * Returns the context object for the request.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
 | 
					  implicit def context: Context = {
 | 
				
			||||||
 | 
					    contextCache.get match {
 | 
				
			||||||
  private def currentURL: String = defining(request.getQueryString){ queryString =>
 | 
					      case null => {
 | 
				
			||||||
    request.getRequestURI + (if(queryString != null) "?" + queryString else "")
 | 
					        val context = Context(loadSystemSettings(), LoginAccount, request)
 | 
				
			||||||
 | 
					        contextCache.set(context)
 | 
				
			||||||
 | 
					        context
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case context => context
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
 | 
					  private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
 | 
				
			||||||
@@ -71,7 +82,7 @@ abstract class ControllerBase extends ScalatraFilter
 | 
				
			|||||||
      action
 | 
					      action
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
 | 
					  override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route =
 | 
				
			||||||
    super.ajaxGet(path, form){ form =>
 | 
					    super.ajaxGet(path, form){ form =>
 | 
				
			||||||
      request.setAttribute(Keys.Request.Ajax, "true")
 | 
					      request.setAttribute(Keys.Request.Ajax, "true")
 | 
				
			||||||
      action(form)
 | 
					      action(form)
 | 
				
			||||||
@@ -83,7 +94,7 @@ abstract class ControllerBase extends ScalatraFilter
 | 
				
			|||||||
      action
 | 
					      action
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
 | 
					  override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route =
 | 
				
			||||||
    super.ajaxPost(path, form){ form =>
 | 
					    super.ajaxPost(path, form){ form =>
 | 
				
			||||||
      request.setAttribute(Keys.Request.Ajax, "true")
 | 
					      request.setAttribute(Keys.Request.Ajax, "true")
 | 
				
			||||||
      action(form)
 | 
					      action(form)
 | 
				
			||||||
@@ -106,27 +117,34 @@ abstract class ControllerBase extends ScalatraFilter
 | 
				
			|||||||
        if(request.getMethod.toUpperCase == "POST"){
 | 
					        if(request.getMethod.toUpperCase == "POST"){
 | 
				
			||||||
          org.scalatra.Unauthorized(redirect("/signin"))
 | 
					          org.scalatra.Unauthorized(redirect("/signin"))
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
 | 
					          org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode(
 | 
				
			||||||
 | 
					            defining(request.getQueryString){ queryString =>
 | 
				
			||||||
 | 
					              request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          )))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected def baseUrl = defining(request.getRequestURL.toString){ url =>
 | 
					  // TODO Scala 2.11
 | 
				
			||||||
    url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
 | 
					  override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
 | 
				
			||||||
  }
 | 
					                   includeContextPath: Boolean = true, includeServletPath: Boolean = true,
 | 
				
			||||||
 | 
					                   absolutize: Boolean = true, withSessionId: Boolean = true)
 | 
				
			||||||
 | 
					                  (implicit request: HttpServletRequest, response: HttpServletResponse): String =
 | 
				
			||||||
 | 
					    if (path.startsWith("http")) path
 | 
				
			||||||
 | 
					    else baseUrl + super.url(path, params, false, false, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Context object for the current request.
 | 
					 * Context object for the current request.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
 | 
					case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def redirectUrl = if(request.getParameter("redirect") != null){
 | 
					  val path = settings.baseUrl.getOrElse(request.getContextPath)
 | 
				
			||||||
    request.getParameter("redirect")
 | 
					  val currentPath = request.getRequestURI.substring(request.getContextPath.length)
 | 
				
			||||||
  } else {
 | 
					  val baseUrl = settings.baseUrl(request)
 | 
				
			||||||
    currentUrl
 | 
					  val host = new java.net.URL(baseUrl).getHost
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get object from cache.
 | 
					   * Get object from cache.
 | 
				
			||||||
@@ -148,7 +166,7 @@ case class Context(path: String, loginAccount: Option[Account], currentUrl: Stri
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Base trait for controllers which manages account information.
 | 
					 * Base trait for controllers which manages account information.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
 | 
					trait AccountManagementControllerBase extends ControllerBase {
 | 
				
			||||||
  self: AccountService  =>
 | 
					  self: AccountService  =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
 | 
					  protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
 | 
				
			||||||
@@ -159,9 +177,9 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      fileId.map { fileId =>
 | 
					      fileId.map { fileId =>
 | 
				
			||||||
        val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
 | 
					        val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get)
 | 
				
			||||||
        FileUtils.moveFile(
 | 
					        FileUtils.moveFile(
 | 
				
			||||||
          getTemporaryFile(fileId),
 | 
					          new java.io.File(getTemporaryDir(session.getId), fileId),
 | 
				
			||||||
          new java.io.File(getUserUploadDir(userName), filename)
 | 
					          new java.io.File(getUserUploadDir(userName), filename)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        updateAvatarImage(userName, Some(filename))
 | 
					        updateAvatarImage(userName, Some(filename))
 | 
				
			||||||
@@ -169,40 +187,15 @@ trait AccountManagementControllerBase extends ControllerBase with FileUploadCont
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected def uniqueUserName: Constraint = new Constraint(){
 | 
					  protected def uniqueUserName: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      getAccountByUserName(value).map { _ => "User already exists." }
 | 
					      getAccountByUserName(value, true).map { _ => "User already exists." }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
 | 
					  protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
 | 
					    override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
 | 
				
			||||||
      getAccountByMailAddress(value)
 | 
					      getAccountByMailAddress(value, true)
 | 
				
			||||||
        .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
 | 
					        .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
 | 
				
			||||||
        .map    { _ => "Mail address is already registered." }
 | 
					        .map    { _ => "Mail address is already registered." }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Base trait for controllers which needs file uploading feature.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
trait FileUploadControllerBase {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def generateFileId: String =
 | 
					 | 
				
			||||||
    new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def TemporaryDir(implicit session: HttpSession): java.io.File =
 | 
					 | 
				
			||||||
    new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
 | 
					 | 
				
			||||||
    new java.io.File(TemporaryDir, fileId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  //  def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
 | 
					 | 
				
			||||||
  //    getTemporaryFile(fileId).delete()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def removeTemporaryFiles()(implicit session: HttpSession): Unit =
 | 
					 | 
				
			||||||
    FileUtils.deleteDirectory(TemporaryDir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
 | 
					 | 
				
			||||||
    session.getAndRemove[String](Keys.Session.Upload(fileId))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,193 +0,0 @@
 | 
				
			|||||||
package app
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import util.Directory._
 | 
					 | 
				
			||||||
import util.ControlUtil._
 | 
					 | 
				
			||||||
import util._
 | 
					 | 
				
			||||||
import service._
 | 
					 | 
				
			||||||
import java.io.File
 | 
					 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					 | 
				
			||||||
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 ReadableUsersAuthenticator
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Creates new repository.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
trait CreateRepositoryControllerBase extends ControllerBase {
 | 
					 | 
				
			||||||
  self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
 | 
					 | 
				
			||||||
    with UsersAuthenticator with ReadableUsersAuthenticator =>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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(getGroupsByUserName(context.loginAccount.get.userName))
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Create new repository.
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  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, form.owner, form.description, form.isPrivate)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Add collaborators for group repository
 | 
					 | 
				
			||||||
        if(ownerAccount.isGroupAccount){
 | 
					 | 
				
			||||||
          getGroupMembers(form.owner).foreach { userName =>
 | 
					 | 
				
			||||||
            addCollaborator(form.owner, form.name, userName)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Insert default labels
 | 
					 | 
				
			||||||
        insertDefaultLabels(form.owner, form.name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Create the actual repository
 | 
					 | 
				
			||||||
        val gitdir = getRepositoryDir(form.owner, form.name)
 | 
					 | 
				
			||||||
        JGitUtil.initRepository(gitdir)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if(form.createReadme){
 | 
					 | 
				
			||||||
          FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
 | 
					 | 
				
			||||||
            // Clone the repository
 | 
					 | 
				
			||||||
            Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).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")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            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}")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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(){
 | 
					 | 
				
			||||||
    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.")
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -49,21 +49,21 @@ trait DashboardControllerBase extends ControllerBase {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val userName   = context.loginAccount.get.userName
 | 
					    val userName   = context.loginAccount.get.userName
 | 
				
			||||||
    val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
 | 
					    val userRepos  = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
 | 
				
			||||||
    val filterUser = Map(filter -> userName)
 | 
					    val filterUser = Map(filter -> userName)
 | 
				
			||||||
    val page = IssueSearchCondition.page(request)
 | 
					    val page = IssueSearchCondition.page(request)
 | 
				
			||||||
    // 
 | 
					
 | 
				
			||||||
    dashboard.html.issues(
 | 
					    dashboard.html.issues(
 | 
				
			||||||
        issues.html.listparts(
 | 
					        issues.html.listparts(
 | 
				
			||||||
            searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
 | 
					            searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
 | 
				
			||||||
            page,
 | 
					            page,
 | 
				
			||||||
            countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
 | 
					            countIssue(condition.copy(state = "open"  ), filterUser, false, userRepos: _*),
 | 
				
			||||||
            countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
 | 
					            countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
 | 
				
			||||||
            condition),
 | 
					            condition),
 | 
				
			||||||
        countIssue(condition, Map.empty, false, repositories: _*),
 | 
					        countIssue(condition, Map.empty, false, userRepos: _*),
 | 
				
			||||||
        countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
 | 
					        countIssue(condition, Map("assigned"   -> userName), false, userRepos: _*),
 | 
				
			||||||
        countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
 | 
					        countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
 | 
				
			||||||
        countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
 | 
					        countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
 | 
				
			||||||
        condition,
 | 
					        condition,
 | 
				
			||||||
        filter)    
 | 
					        filter)    
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -80,25 +80,26 @@ trait DashboardControllerBase extends ControllerBase {
 | 
				
			|||||||
    }.copy(repo = repository))
 | 
					    }.copy(repo = repository))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val userName   = context.loginAccount.get.userName
 | 
					    val userName   = context.loginAccount.get.userName
 | 
				
			||||||
    val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
 | 
					    val allRepos   = getAllRepositories()
 | 
				
			||||||
 | 
					    val userRepos  = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
 | 
				
			||||||
    val filterUser = Map(filter -> userName)
 | 
					    val filterUser = Map(filter -> userName)
 | 
				
			||||||
    val page = IssueSearchCondition.page(request)
 | 
					    val page = IssueSearchCondition.page(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val counts = countIssueGroupByRepository(
 | 
					    val counts = countIssueGroupByRepository(
 | 
				
			||||||
      IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
 | 
					      IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dashboard.html.pulls(
 | 
					    dashboard.html.pulls(
 | 
				
			||||||
      pulls.html.listparts(
 | 
					      pulls.html.listparts(
 | 
				
			||||||
        searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
 | 
					        searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
 | 
				
			||||||
        page,
 | 
					        page,
 | 
				
			||||||
        countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
 | 
					        countIssue(condition.copy(state = "open"  ), filterUser, true, allRepos: _*),
 | 
				
			||||||
        countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
 | 
					        countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
 | 
				
			||||||
        condition,
 | 
					        condition,
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
        false),
 | 
					        false),
 | 
				
			||||||
      getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
 | 
					      getPullRequestCountGroupByUser(condition.state == "closed", None, None),
 | 
				
			||||||
      getRepositoryNamesOfUser(userName).map { RepoName =>
 | 
					      userRepos.map { case (userName, repoName) =>
 | 
				
			||||||
        (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
 | 
					        (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
 | 
				
			||||||
      }.sortBy(_._3).reverse,
 | 
					      }.sortBy(_._3).reverse,
 | 
				
			||||||
      condition,
 | 
					      condition,
 | 
				
			||||||
      filter)
 | 
					      filter)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +1,44 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import _root_.util.{Keys, FileUtil}
 | 
					import util.{Keys, FileUtil}
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
import org.scalatra._
 | 
					import org.scalatra._
 | 
				
			||||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
 | 
					import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides Ajax based file upload functionality.
 | 
					 * Provides Ajax based file upload functionality.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * This servlet saves uploaded file as temporary file and returns the unique id.
 | 
					 * This servlet saves uploaded file.
 | 
				
			||||||
 * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class FileUploadController extends ScalatraServlet
 | 
					class FileUploadController extends ScalatraServlet with FileUploadSupport {
 | 
				
			||||||
  with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
 | 
					  configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/image"){
 | 
					  post("/image"){
 | 
				
			||||||
    fileParams.get("file") match {
 | 
					    execute { (file, fileId) =>
 | 
				
			||||||
      case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
 | 
					      FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
 | 
				
			||||||
        FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
 | 
					 | 
				
			||||||
      session += Keys.Session.Upload(fileId) -> file.name
 | 
					      session += Keys.Session.Upload(fileId) -> file.name
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/image/:owner/:repository"){
 | 
				
			||||||
 | 
					    execute { (file, fileId) =>
 | 
				
			||||||
 | 
					      FileUtils.writeByteArrayToFile(new java.io.File(
 | 
				
			||||||
 | 
					        getAttachedDir(params("owner"), params("repository")),
 | 
				
			||||||
 | 
					        fileId + "." + FileUtil.getExtension(file.getName)), file.get)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
 | 
				
			||||||
 | 
					    case Some(file) if(FileUtil.isImage(file.name)) =>
 | 
				
			||||||
 | 
					      defining(FileUtil.generateFileId){ fileId =>
 | 
				
			||||||
 | 
					        f(file, fileId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(fileId)
 | 
					        Ok(fileId)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case None => BadRequest
 | 
					    case _ => BadRequest
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,38 +1,106 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import util._
 | 
					import util._
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IndexController extends IndexControllerBase 
 | 
					class IndexController extends IndexControllerBase 
 | 
				
			||||||
  with RepositoryService with SystemSettingsService with ActivityService with AccountService
 | 
					  with RepositoryService with ActivityService with AccountService with UsersAuthenticator
 | 
				
			||||||
with UsersAuthenticator
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait IndexControllerBase extends ControllerBase {
 | 
					trait IndexControllerBase extends ControllerBase {
 | 
				
			||||||
  self: RepositoryService with SystemSettingsService with ActivityService with AccountService
 | 
					  self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
 | 
				
			||||||
  with UsersAuthenticator =>
 | 
					
 | 
				
			||||||
 | 
					  case class SignInForm(userName: String, password: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val form = mapping(
 | 
				
			||||||
 | 
					    "userName" -> trim(label("Username", text(required))),
 | 
				
			||||||
 | 
					    "password" -> trim(label("Password", text(required)))
 | 
				
			||||||
 | 
					  )(SignInForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/"){
 | 
					  get("/"){
 | 
				
			||||||
    val loginAccount = context.loginAccount
 | 
					    val loginAccount = context.loginAccount
 | 
				
			||||||
 | 
					    if(loginAccount.isEmpty) {
 | 
				
			||||||
        html.index(getRecentActivities(),
 | 
					        html.index(getRecentActivities(),
 | 
				
			||||||
      getVisibleRepositories(loginAccount, baseUrl),
 | 
					            getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
 | 
				
			||||||
      loadSystemSettings(),
 | 
					            loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
 | 
				
			||||||
      loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        val loginUserName = loginAccount.get.userName
 | 
				
			||||||
 | 
					        val loginUserGroups = getGroupsByUserName(loginUserName)
 | 
				
			||||||
 | 
					        var visibleOwnerSet : Set[String] = Set(loginUserName)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        visibleOwnerSet ++= loginUserGroups
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        html.index(getRecentActivitiesByOwners(visibleOwnerSet),
 | 
				
			||||||
 | 
					            getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
 | 
				
			||||||
 | 
					            loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) 
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/signin"){
 | 
				
			||||||
 | 
					    val redirect = params.get("redirect")
 | 
				
			||||||
 | 
					    if(redirect.isDefined && redirect.get.startsWith("/")){
 | 
				
			||||||
 | 
					      flash += Keys.Flash.Redirect -> redirect.get
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    html.signin()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/signin", form){ form =>
 | 
				
			||||||
 | 
					    authenticate(context.settings, form.userName, form.password) match {
 | 
				
			||||||
 | 
					      case Some(account) => signin(account)
 | 
				
			||||||
 | 
					      case None          => redirect("/signin")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/signout"){
 | 
				
			||||||
 | 
					    session.invalidate
 | 
				
			||||||
 | 
					    redirect("/")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/activities.atom"){
 | 
				
			||||||
 | 
					    contentType = "application/atom+xml; type=feed"
 | 
				
			||||||
 | 
					    helper.xml.feed(getRecentActivities())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Set account information into HttpSession and redirect.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def signin(account: model.Account) = {
 | 
				
			||||||
 | 
					    session.setAttribute(Keys.Session.LoginAccount, account)
 | 
				
			||||||
 | 
					    updateLastLoginDate(account.userName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(LDAPUtil.isDummyMailAddress(account)) {
 | 
				
			||||||
 | 
					      redirect("/" + account.userName + "/_edit")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl =>
 | 
				
			||||||
 | 
					      if(redirectUrl.stripSuffix("/") == request.getContextPath){
 | 
				
			||||||
 | 
					        redirect("/")
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        redirect(redirectUrl)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }.getOrElse {
 | 
				
			||||||
 | 
					      redirect("/")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * JSON API for collaborator completion.
 | 
					   * JSON API for collaborator completion.
 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * TODO Move to other controller?
 | 
					 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  get("/_user/proposals")(usersOnly {
 | 
					  get("/_user/proposals")(usersOnly {
 | 
				
			||||||
    contentType = formats("json")
 | 
					    contentType = formats("json")
 | 
				
			||||||
    org.json4s.jackson.Serialization.write(
 | 
					    org.json4s.jackson.Serialization.write(
 | 
				
			||||||
      Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
 | 
					      Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * JSON APU for checking user existence.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  post("/_user/existence")(usersOnly {
 | 
				
			||||||
 | 
					    getAccountByUserName(params("userName")).isDefined
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,12 @@ import jp.sf.amateras.scalatra.forms._
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import IssuesService._
 | 
					import IssuesService._
 | 
				
			||||||
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
 | 
					import util._
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import org.scalatra.Ok
 | 
					import org.scalatra.Ok
 | 
				
			||||||
 | 
					import model.Issue
 | 
				
			||||||
 | 
					import plugin.PluginSystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IssuesController extends IssuesControllerBase
 | 
					class IssuesController extends IssuesControllerBase
 | 
				
			||||||
  with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
 | 
					  with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
 | 
				
			||||||
@@ -110,9 +112,14 @@ trait IssuesControllerBase extends ControllerBase {
 | 
				
			|||||||
      // record activity
 | 
					      // record activity
 | 
				
			||||||
      recordCreateIssueActivity(owner, name, userName, issueId, form.title)
 | 
					      recordCreateIssueActivity(owner, name, userName, issueId, form.title)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // extract references and create refer comment
 | 
				
			||||||
 | 
					      getIssue(owner, name, issueId.toString).foreach { issue =>
 | 
				
			||||||
 | 
					        createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // notifications
 | 
					      // notifications
 | 
				
			||||||
      Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
 | 
					      Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
 | 
				
			||||||
        Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
 | 
					        Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      redirect(s"/${owner}/${name}/issues/${issueId}")
 | 
					      redirect(s"/${owner}/${name}/issues/${issueId}")
 | 
				
			||||||
@@ -123,7 +130,11 @@ trait IssuesControllerBase extends ControllerBase {
 | 
				
			|||||||
    defining(repository.owner, repository.name){ case (owner, name) =>
 | 
					    defining(repository.owner, repository.name){ case (owner, name) =>
 | 
				
			||||||
      getIssue(owner, name, params("id")).map { issue =>
 | 
					      getIssue(owner, name, params("id")).map { issue =>
 | 
				
			||||||
        if(isEditable(owner, name, issue.openedUserName)){
 | 
					        if(isEditable(owner, name, issue.openedUserName)){
 | 
				
			||||||
 | 
					          // update issue
 | 
				
			||||||
          updateIssue(owner, name, issue.issueId, form.title, form.content)
 | 
					          updateIssue(owner, name, issue.issueId, form.title, form.content)
 | 
				
			||||||
 | 
					          // extract references and create refer comment
 | 
				
			||||||
 | 
					          createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
 | 
					          redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
 | 
				
			||||||
        } else Unauthorized
 | 
					        } else Unauthorized
 | 
				
			||||||
      } getOrElse NotFound
 | 
					      } getOrElse NotFound
 | 
				
			||||||
@@ -263,6 +274,17 @@ trait IssuesControllerBase extends ControllerBase {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
 | 
				
			||||||
 | 
					    (Directory.getAttachedDir(repository.owner, repository.name) match {
 | 
				
			||||||
 | 
					      case dir if(dir.exists && dir.isDirectory) =>
 | 
				
			||||||
 | 
					        dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
 | 
				
			||||||
 | 
					          contentType = FileUtil.getMimeType(file.getName)
 | 
				
			||||||
 | 
					          file
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      case _ => None
 | 
				
			||||||
 | 
					    }) getOrElse NotFound
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
 | 
					  val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
 | 
				
			||||||
  val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
 | 
					  val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -274,6 +296,15 @@ trait IssuesControllerBase extends ControllerBase {
 | 
				
			|||||||
    redirect(s"/${repository.owner}/${repository.name}/issues")
 | 
					    redirect(s"/${repository.owner}/${repository.name}/issues")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
 | 
				
			||||||
 | 
					    StringUtil.extractIssueId(message).foreach { issueId =>
 | 
				
			||||||
 | 
					      if(getIssue(owner, repository, issueId).isDefined){
 | 
				
			||||||
 | 
					        createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
 | 
				
			||||||
 | 
					                      fromIssue.issueId + ":" + fromIssue.title, "refer")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
 | 
					   * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -313,18 +344,23 @@ trait IssuesControllerBase extends ControllerBase {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
 | 
					        recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // extract references and create refer comment
 | 
				
			||||||
 | 
					        content.map { content =>
 | 
				
			||||||
 | 
					          createReferComment(owner, name, issue, content)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // notifications
 | 
					        // notifications
 | 
				
			||||||
        Notifier() match {
 | 
					        Notifier() match {
 | 
				
			||||||
          case f =>
 | 
					          case f =>
 | 
				
			||||||
            content foreach {
 | 
					            content foreach {
 | 
				
			||||||
              f.toNotify(repository, issueId, _){
 | 
					              f.toNotify(repository, issueId, _){
 | 
				
			||||||
                Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
 | 
					                Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
 | 
				
			||||||
                  if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
 | 
					                  if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            action foreach {
 | 
					            action foreach {
 | 
				
			||||||
              f.toNotify(repository, issueId, _){
 | 
					              f.toNotify(repository, issueId, _){
 | 
				
			||||||
                Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
 | 
					                Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ package app
 | 
				
			|||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import util.CollaboratorsAuthenticator
 | 
					import util.CollaboratorsAuthenticator
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LabelsController extends LabelsControllerBase
 | 
					class LabelsController extends LabelsControllerBase
 | 
				
			||||||
  with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
 | 
					  with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
 | 
				
			||||||
@@ -51,8 +53,8 @@ trait LabelsControllerBase extends ControllerBase {
 | 
				
			|||||||
   * Constraint for the identifier such as user name, repository name or page name.
 | 
					   * Constraint for the identifier such as user name, repository name or page name.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def labelName: Constraint = new Constraint(){
 | 
					  private def labelName: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      if(!value.matches("^[^,]+$")){
 | 
					      if(value.contains(',')){
 | 
				
			||||||
        Some(s"${name} contains invalid character.")
 | 
					        Some(s"${name} contains invalid character.")
 | 
				
			||||||
      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
					      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
				
			||||||
        Some(s"${name} starts with invalid character.")
 | 
					        Some(s"${name} starts with invalid character.")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,28 +4,30 @@ import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticat
 | 
				
			|||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import util.FileUtil._
 | 
					 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.eclipse.jgit.transport.RefSpec
 | 
					import org.eclipse.jgit.transport.RefSpec
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					 | 
				
			||||||
import scala.collection.JavaConverters._
 | 
					import scala.collection.JavaConverters._
 | 
				
			||||||
import org.eclipse.jgit.lib.PersonIdent
 | 
					import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
 | 
				
			||||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode
 | 
					 | 
				
			||||||
import service.IssuesService._
 | 
					import service.IssuesService._
 | 
				
			||||||
import service.PullRequestService._
 | 
					import service.PullRequestService._
 | 
				
			||||||
import util.JGitUtil.DiffInfo
 | 
					import util.JGitUtil.DiffInfo
 | 
				
			||||||
import service.RepositoryService.RepositoryTreeNode
 | 
					 | 
				
			||||||
import util.JGitUtil.CommitInfo
 | 
					import util.JGitUtil.CommitInfo
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import org.eclipse.jgit.merge.MergeStrategy
 | 
				
			||||||
 | 
					import org.eclipse.jgit.errors.NoMergeBaseException
 | 
				
			||||||
 | 
					import service.WebHookService.WebHookPayload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PullRequestsController extends PullRequestsControllerBase
 | 
					class PullRequestsController extends PullRequestsControllerBase
 | 
				
			||||||
  with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
 | 
					  with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
 | 
				
			||||||
  with ReferrerAuthenticator with CollaboratorsAuthenticator
 | 
					  with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait PullRequestsControllerBase extends ControllerBase {
 | 
					trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			||||||
  self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService
 | 
					  self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
 | 
				
			||||||
    with ReferrerAuthenticator with CollaboratorsAuthenticator =>
 | 
					    with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val pullRequestForm = mapping(
 | 
					  val pullRequestForm = mapping(
 | 
				
			||||||
    "title"                 -> trim(label("Title"  , text(required, maxlength(100)))),
 | 
					    "title"                 -> trim(label("Title"  , text(required, maxlength(100)))),
 | 
				
			||||||
@@ -33,6 +35,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
    "targetUserName"        -> trim(text(required, maxlength(100))),
 | 
					    "targetUserName"        -> trim(text(required, maxlength(100))),
 | 
				
			||||||
    "targetBranch"          -> trim(text(required, maxlength(100))),
 | 
					    "targetBranch"          -> trim(text(required, maxlength(100))),
 | 
				
			||||||
    "requestUserName"       -> trim(text(required, maxlength(100))),
 | 
					    "requestUserName"       -> trim(text(required, maxlength(100))),
 | 
				
			||||||
 | 
					    "requestRepositoryName" -> trim(text(required, maxlength(100))),
 | 
				
			||||||
    "requestBranch"         -> trim(text(required, maxlength(100))),
 | 
					    "requestBranch"         -> trim(text(required, maxlength(100))),
 | 
				
			||||||
    "commitIdFrom"          -> trim(text(required, maxlength(40))),
 | 
					    "commitIdFrom"          -> trim(text(required, maxlength(40))),
 | 
				
			||||||
    "commitIdTo"            -> trim(text(required, maxlength(40)))
 | 
					    "commitIdTo"            -> trim(text(required, maxlength(40)))
 | 
				
			||||||
@@ -48,6 +51,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
    targetUserName: String,
 | 
					    targetUserName: String,
 | 
				
			||||||
    targetBranch: String,
 | 
					    targetBranch: String,
 | 
				
			||||||
    requestUserName: String,
 | 
					    requestUserName: String,
 | 
				
			||||||
 | 
					    requestRepositoryName: String,
 | 
				
			||||||
    requestBranch: String,
 | 
					    requestBranch: String,
 | 
				
			||||||
    commitIdFrom: String,
 | 
					    commitIdFrom: String,
 | 
				
			||||||
    commitIdTo: String)
 | 
					    commitIdTo: String)
 | 
				
			||||||
@@ -74,8 +78,10 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
          pulls.html.pullreq(
 | 
					          pulls.html.pullreq(
 | 
				
			||||||
            issue, pullreq,
 | 
					            issue, pullreq,
 | 
				
			||||||
            getComments(owner, name, issueId),
 | 
					            getComments(owner, name, issueId),
 | 
				
			||||||
 | 
					            getIssueLabels(owner, name, issueId),
 | 
				
			||||||
            (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
 | 
					            (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
 | 
				
			||||||
            getMilestonesWithIssueCount(owner, name),
 | 
					            getMilestonesWithIssueCount(owner, name),
 | 
				
			||||||
 | 
					            getLabels(owner, name),
 | 
				
			||||||
            commits,
 | 
					            commits,
 | 
				
			||||||
            diffs,
 | 
					            diffs,
 | 
				
			||||||
            hasWritePermission(owner, name, context.loginAccount),
 | 
					            hasWritePermission(owner, name, context.loginAccount),
 | 
				
			||||||
@@ -91,23 +97,35 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
      val name  = repository.name
 | 
					      val name  = repository.name
 | 
				
			||||||
      getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
 | 
					      getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
 | 
				
			||||||
        pulls.html.mergeguide(
 | 
					        pulls.html.mergeguide(
 | 
				
			||||||
          checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
 | 
					          checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
 | 
				
			||||||
          pullreq,
 | 
					          pullreq,
 | 
				
			||||||
          s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
 | 
					          s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } getOrElse NotFound
 | 
					    } getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
 | 
				
			||||||
 | 
					    params("id").toIntOpt.map { issueId =>
 | 
				
			||||||
 | 
					      val branchName = multiParams("splat").head
 | 
				
			||||||
 | 
					      val userName   = context.loginAccount.get.userName
 | 
				
			||||||
 | 
					      if(repository.repository.defaultBranch != branchName){
 | 
				
			||||||
 | 
					        using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					          git.branchDelete().setForce(true).setBranchNames(branchName).call()
 | 
				
			||||||
 | 
					          recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
 | 
				
			||||||
 | 
					      redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
 | 
				
			||||||
 | 
					    } getOrElse NotFound
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
 | 
					  post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
 | 
				
			||||||
    params("id").toIntOpt.flatMap{ issueId =>
 | 
					    params("id").toIntOpt.flatMap { issueId =>
 | 
				
			||||||
      val owner = repository.owner
 | 
					      val owner = repository.owner
 | 
				
			||||||
      val name  = repository.name
 | 
					      val name  = repository.name
 | 
				
			||||||
      LockUtil.lock(s"${owner}/${name}/merge"){
 | 
					      LockUtil.lock(s"${owner}/${name}"){
 | 
				
			||||||
        getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
 | 
					        getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
 | 
				
			||||||
          val remote = getRepositoryDir(owner, name)
 | 
					          using(Git.open(getRepositoryDir(owner, name))) { git =>
 | 
				
			||||||
          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.
 | 
					            // mark issue as merged and close.
 | 
				
			||||||
            val loginAccount = context.loginAccount.get
 | 
					            val loginAccount = context.loginAccount.get
 | 
				
			||||||
            createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
 | 
					            createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
 | 
				
			||||||
@@ -117,64 +135,85 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
            // record activity
 | 
					            // record activity
 | 
				
			||||||
            recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
 | 
					            recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // fetch pull request to temporary working repository
 | 
					            // merge
 | 
				
			||||||
              val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
 | 
					            val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
 | 
				
			||||||
 | 
					            val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
 | 
				
			||||||
              git.fetch
 | 
					            val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
 | 
				
			||||||
                .setRemote(getRepositoryDir(owner, name).toURI.toString)
 | 
					            val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
 | 
				
			||||||
                .setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
 | 
					            val conflicted = try {
 | 
				
			||||||
 | 
					              !merger.merge(mergeBaseTip, mergeTip)
 | 
				
			||||||
              // merge pull request
 | 
					            } catch {
 | 
				
			||||||
              git.checkout.setName(pullreq.branch).call
 | 
					              case e: NoMergeBaseException => true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
              val result = git.merge
 | 
					            if (conflicted) {
 | 
				
			||||||
                .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.")
 | 
					              throw new RuntimeException("This pull request can't merge automatically.")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // merge commit
 | 
					            // creates merge commit
 | 
				
			||||||
              git.getRepository.writeMergeCommitMsg(
 | 
					            val mergeCommit = new CommitBuilder()
 | 
				
			||||||
                s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
 | 
					            mergeCommit.setTreeId(merger.getResultTreeId)
 | 
				
			||||||
                + form.message)
 | 
					            mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
 | 
				
			||||||
 | 
					            val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
 | 
				
			||||||
 | 
					            mergeCommit.setAuthor(personIdent)
 | 
				
			||||||
 | 
					            mergeCommit.setCommitter(personIdent)
 | 
				
			||||||
 | 
					            mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
 | 
				
			||||||
 | 
					                                   form.message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              git.commit
 | 
					            // insertObject and got mergeCommit Object Id
 | 
				
			||||||
                .setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
 | 
					            val inserter = git.getRepository.newObjectInserter
 | 
				
			||||||
                .call
 | 
					            val mergeCommitId = inserter.insert(mergeCommit)
 | 
				
			||||||
 | 
					            inserter.flush()
 | 
				
			||||||
 | 
					            inserter.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              // push
 | 
					            // update refs
 | 
				
			||||||
              git.push.call
 | 
					            val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
 | 
				
			||||||
 | 
					            refUpdate.setNewObjectId(mergeCommitId)
 | 
				
			||||||
 | 
					            refUpdate.setForceUpdate(false)
 | 
				
			||||||
 | 
					            refUpdate.setRefLogIdent(personIdent)
 | 
				
			||||||
 | 
					            refUpdate.setRefLogMessage("merged", true)
 | 
				
			||||||
 | 
					            refUpdate.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
 | 
					            val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
 | 
				
			||||||
              pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
 | 
					              pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // close issue by content of pull request
 | 
				
			||||||
 | 
					            val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
 | 
				
			||||||
 | 
					            if(pullreq.branch == defaultBranch){
 | 
				
			||||||
              commits.flatten.foreach { commit =>
 | 
					              commits.flatten.foreach { commit =>
 | 
				
			||||||
                if(!existsCommitId(owner, name, commit.id)){
 | 
					                closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
 | 
				
			||||||
                  insertCommitId(owner, name, commit.id)
 | 
					 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					              issue.content match {
 | 
				
			||||||
 | 
					                case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
 | 
				
			||||||
 | 
					                case _ =>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // call web hook
 | 
				
			||||||
 | 
					            getWebHookURLs(owner, name) match {
 | 
				
			||||||
 | 
					              case webHookURLs if(webHookURLs.nonEmpty) =>
 | 
				
			||||||
 | 
					                for(ownerAccount <- getAccountByUserName(owner)){
 | 
				
			||||||
 | 
					                  callWebHook(owner, name, webHookURLs,
 | 
				
			||||||
 | 
					                    WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              case _ =>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // notifications
 | 
					            // notifications
 | 
				
			||||||
            Notifier().toNotify(repository, issueId, "merge"){
 | 
					            Notifier().toNotify(repository, issueId, "merge"){
 | 
				
			||||||
                Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
 | 
					              Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            redirect(s"/${owner}/${name}/pull/${issueId}")
 | 
					            redirect(s"/${owner}/${name}/pull/${issueId}")
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } getOrElse NotFound
 | 
					    } getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
 | 
					  get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
 | 
				
			||||||
    (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
 | 
					    (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
 | 
				
			||||||
      case (Some(originUserName), Some(originRepositoryName)) => {
 | 
					      case (Some(originUserName), Some(originRepositoryName)) => {
 | 
				
			||||||
        getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
 | 
					        getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
 | 
				
			||||||
          using(
 | 
					          using(
 | 
				
			||||||
            Git.open(getRepositoryDir(originUserName, originRepositoryName)),
 | 
					            Git.open(getRepositoryDir(originUserName, originRepositoryName)),
 | 
				
			||||||
            Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
 | 
					            Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
 | 
				
			||||||
@@ -182,89 +221,101 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
            val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
 | 
					            val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
 | 
				
			||||||
            val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
 | 
					            val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
 | 
					            redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } getOrElse NotFound
 | 
					        } getOrElse NotFound
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case _ => {
 | 
					      case _ => {
 | 
				
			||||||
        using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
 | 
					        using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
 | 
				
			||||||
          JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
 | 
					          JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
 | 
				
			||||||
            redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
 | 
					            redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
 | 
				
			||||||
          } getOrElse {
 | 
					          } getOrElse {
 | 
				
			||||||
            redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
 | 
					            redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
 | 
					  get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
 | 
				
			||||||
    val Seq(origin, forked) = multiParams("splat")
 | 
					    val Seq(origin, forked) = multiParams("splat")
 | 
				
			||||||
    val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
 | 
					    val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
 | 
				
			||||||
    val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
 | 
					    val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    (getRepository(originOwner, repository.name, baseUrl),
 | 
					    (for(
 | 
				
			||||||
     getRepository(forkedOwner, repository.name, baseUrl)) match {
 | 
					      originRepositoryName <- if(originOwner == forkedOwner){
 | 
				
			||||||
      case (Some(originRepository), Some(forkedRepository)) => {
 | 
					        Some(forkedRepository.name)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        forkedRepository.repository.originRepositoryName.orElse {
 | 
				
			||||||
 | 
					          getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
 | 
				
			||||||
 | 
					    ) yield {
 | 
				
			||||||
      using(
 | 
					      using(
 | 
				
			||||||
          Git.open(getRepositoryDir(originOwner, repository.name)),
 | 
					        Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
 | 
				
			||||||
          Git.open(getRepositoryDir(forkedOwner, repository.name))
 | 
					        Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
 | 
				
			||||||
      ){ case (oldGit, newGit) =>
 | 
					      ){ case (oldGit, newGit) =>
 | 
				
			||||||
        val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
 | 
					        val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
 | 
				
			||||||
        val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
 | 
					        val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          val forkedId = getForkedCommitId(oldGit, newGit,
 | 
					        val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
 | 
				
			||||||
            originOwner, repository.name, originBranch,
 | 
					          originRepository.owner, originRepository.name, originBranch,
 | 
				
			||||||
            forkedOwner, repository.name, forkedBranch)
 | 
					          forkedRepository.owner, forkedRepository.name, forkedBranch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val oldId = oldGit.getRepository.resolve(forkedId)
 | 
					        val oldId = oldGit.getRepository.resolve(forkedId)
 | 
				
			||||||
        val newId = newGit.getRepository.resolve(forkedBranch)
 | 
					        val newId = newGit.getRepository.resolve(forkedBranch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val (commits, diffs) = getRequestCompareInfo(
 | 
					        val (commits, diffs) = getRequestCompareInfo(
 | 
				
			||||||
            originOwner, repository.name, oldId.getName,
 | 
					          originRepository.owner, originRepository.name, oldId.getName,
 | 
				
			||||||
            forkedOwner, repository.name, newId.getName)
 | 
					          forkedRepository.owner, forkedRepository.name, newId.getName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pulls.html.compare(
 | 
					        pulls.html.compare(
 | 
				
			||||||
          commits,
 | 
					          commits,
 | 
				
			||||||
          diffs,
 | 
					          diffs,
 | 
				
			||||||
            repository.repository.originUserName.map { userName =>
 | 
					          (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
 | 
				
			||||||
              userName :: getForkedRepositories(userName, repository.name)
 | 
					            case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
 | 
				
			||||||
            } getOrElse List(repository.owner),
 | 
					            case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          originBranch,
 | 
					          originBranch,
 | 
				
			||||||
          forkedBranch,
 | 
					          forkedBranch,
 | 
				
			||||||
          oldId.getName,
 | 
					          oldId.getName,
 | 
				
			||||||
          newId.getName,
 | 
					          newId.getName,
 | 
				
			||||||
            repository,
 | 
					          forkedRepository,
 | 
				
			||||||
          originRepository,
 | 
					          originRepository,
 | 
				
			||||||
          forkedRepository,
 | 
					          forkedRepository,
 | 
				
			||||||
            hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
					          hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      case _ => NotFound
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }) getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
 | 
					  ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
 | 
				
			||||||
    val Seq(origin, forked) = multiParams("splat")
 | 
					    val Seq(origin, forked) = multiParams("splat")
 | 
				
			||||||
    val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
 | 
					    val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
 | 
				
			||||||
    val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
 | 
					    val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    (getRepository(originOwner, repository.name, baseUrl),
 | 
					    (for(
 | 
				
			||||||
      getRepository(forkedOwner, repository.name, baseUrl)) match {
 | 
					      originRepositoryName <- if(originOwner == forkedOwner){
 | 
				
			||||||
      case (Some(originRepository), Some(forkedRepository)) => {
 | 
					        Some(forkedRepository.name)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        forkedRepository.repository.originRepositoryName.orElse {
 | 
				
			||||||
 | 
					          getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
 | 
				
			||||||
 | 
					    ) yield {
 | 
				
			||||||
      using(
 | 
					      using(
 | 
				
			||||||
          Git.open(getRepositoryDir(originOwner, repository.name)),
 | 
					        Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
 | 
				
			||||||
          Git.open(getRepositoryDir(forkedOwner, repository.name))
 | 
					        Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
 | 
				
			||||||
      ){ case (oldGit, newGit) =>
 | 
					      ){ case (oldGit, newGit) =>
 | 
				
			||||||
        val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
 | 
					        val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
 | 
				
			||||||
        val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
 | 
					        val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pulls.html.mergecheck(
 | 
					        pulls.html.mergecheck(
 | 
				
			||||||
            checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
 | 
					          checkConflict(originRepository.owner, originRepository.name, originBranch,
 | 
				
			||||||
        }
 | 
					                        forkedRepository.owner, forkedRepository.name, forkedBranch))
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      case _ => NotFound()
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }) getOrElse NotFound
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
 | 
					  post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
 | 
				
			||||||
@@ -286,7 +337,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
      issueId               = issueId,
 | 
					      issueId               = issueId,
 | 
				
			||||||
      originBranch          = form.targetBranch,
 | 
					      originBranch          = form.targetBranch,
 | 
				
			||||||
      requestUserName       = form.requestUserName,
 | 
					      requestUserName       = form.requestUserName,
 | 
				
			||||||
      requestRepositoryName = repository.name,
 | 
					      requestRepositoryName = form.requestRepositoryName,
 | 
				
			||||||
      requestBranch         = form.requestBranch,
 | 
					      requestBranch         = form.requestBranch,
 | 
				
			||||||
      commitIdFrom          = form.commitIdFrom,
 | 
					      commitIdFrom          = form.commitIdFrom,
 | 
				
			||||||
      commitIdTo            = form.commitIdTo)
 | 
					      commitIdTo            = form.commitIdTo)
 | 
				
			||||||
@@ -294,7 +345,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
    // fetch requested branch
 | 
					    // fetch requested branch
 | 
				
			||||||
    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
					    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
      git.fetch
 | 
					      git.fetch
 | 
				
			||||||
        .setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
 | 
					        .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
 | 
				
			||||||
        .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
 | 
					        .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
 | 
				
			||||||
        .call
 | 
					        .call
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -304,7 +355,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // notifications
 | 
					    // notifications
 | 
				
			||||||
    Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
 | 
					    Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
 | 
				
			||||||
      Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
 | 
					      Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
 | 
					    redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
 | 
				
			||||||
@@ -315,23 +366,52 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  private def checkConflict(userName: String, repositoryName: String, branch: String,
 | 
					  private def checkConflict(userName: String, repositoryName: String, branch: String,
 | 
				
			||||||
                            requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
 | 
					                            requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
 | 
				
			||||||
    // TODO Are there more quick way?
 | 
					    LockUtil.lock(s"${userName}/${repositoryName}"){
 | 
				
			||||||
    LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
 | 
					      using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
 | 
				
			||||||
      val remote = getRepositoryDir(userName, repositoryName)
 | 
					        val remoteRefName = s"refs/heads/${branch}"
 | 
				
			||||||
      withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
 | 
					        val tmpRefName = s"refs/merge-check/${userName}/${branch}"
 | 
				
			||||||
        using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
 | 
					        val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
          git.checkout.setName(branch).call
 | 
					          // fetch objects from origin repository branch
 | 
				
			||||||
 | 
					 | 
				
			||||||
          git.fetch
 | 
					          git.fetch
 | 
				
			||||||
            .setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
 | 
					             .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
 | 
				
			||||||
            .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
 | 
					             .setRefSpecs(refSpec)
 | 
				
			||||||
 | 
					             .call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          val result = git.merge
 | 
					          // merge conflict check
 | 
				
			||||||
            .include(git.getRepository.resolve("FETCH_HEAD"))
 | 
					          val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
 | 
				
			||||||
            .setCommit(false).call
 | 
					          val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
 | 
				
			||||||
 | 
					          val mergeTip = git.getRepository.resolve(tmpRefName)
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            !merger.merge(mergeBaseTip, mergeTip)
 | 
				
			||||||
 | 
					          } catch {
 | 
				
			||||||
 | 
					            case e: NoMergeBaseException =>  true
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
 | 
				
			||||||
 | 
					          refUpdate.setForceUpdate(true)
 | 
				
			||||||
 | 
					          refUpdate.delete()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          result.getConflicts != null
 | 
					  /**
 | 
				
			||||||
 | 
					   * Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
 | 
				
			||||||
 | 
					                                         requestUserName: String, requestRepositoryName: String, requestBranch: String,
 | 
				
			||||||
 | 
					                                         issueId: Int): Boolean = {
 | 
				
			||||||
 | 
					    LockUtil.lock(s"${userName}/${repositoryName}") {
 | 
				
			||||||
 | 
					      using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
 | 
				
			||||||
 | 
					        // merge
 | 
				
			||||||
 | 
					        val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
 | 
				
			||||||
 | 
					        val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
 | 
				
			||||||
 | 
					        val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          !merger.merge(mergeBaseTip, mergeTip)
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					          case e: NoMergeBaseException => true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -351,25 +431,8 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
      (defaultOwner, value)
 | 
					      (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,
 | 
					  private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
 | 
				
			||||||
      requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
 | 
					      requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
 | 
				
			||||||
 | 
					 | 
				
			||||||
    using(
 | 
					    using(
 | 
				
			||||||
      Git.open(getRepositoryDir(userName, repositoryName)),
 | 
					      Git.open(getRepositoryDir(userName, repositoryName)),
 | 
				
			||||||
      Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
 | 
					      Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
 | 
				
			||||||
@@ -379,15 +442,14 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
 | 
					      val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
 | 
				
			||||||
        new CommitInfo(revCommit)
 | 
					        new CommitInfo(revCommit)
 | 
				
			||||||
      }.toList.splitWith{ (commit1, commit2) =>
 | 
					      }.toList.splitWith { (commit1, commit2) =>
 | 
				
			||||||
        view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
 | 
					        view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
 | 
					      val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      (commits, diffs)
 | 
					      (commits, diffs)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
 | 
					  private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
 | 
				
			||||||
    defining(repository.owner, repository.name){ case (owner, repoName) =>
 | 
					    defining(repository.owner, repository.name){ case (owner, repoName) =>
 | 
				
			||||||
@@ -403,7 +465,7 @@ trait PullRequestsControllerBase extends ControllerBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      pulls.html.list(
 | 
					      pulls.html.list(
 | 
				
			||||||
        searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
 | 
					        searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
 | 
				
			||||||
        getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
 | 
					        getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
 | 
				
			||||||
        userName,
 | 
					        userName,
 | 
				
			||||||
        page,
 | 
					        page,
 | 
				
			||||||
        countIssue(condition.copy(state = "open"  ), filterUser, true, owner -> repoName),
 | 
					        countIssue(condition.copy(state = "open"  ), filterUser, true, owner -> repoName),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,13 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.{UsersAuthenticator, OwnerAuthenticator}
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
 | 
					import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
 | 
				
			||||||
 | 
					import util.JGitUtil.CommitInfo
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
import org.scalatra.FlashMapSupport
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
import service.WebHookService.WebHookPayload
 | 
					import service.WebHookService.WebHookPayload
 | 
				
			||||||
import util.JGitUtil.CommitInfo
 | 
					import util.JGitUtil.CommitInfo
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
@@ -15,14 +18,15 @@ class RepositorySettingsController extends RepositorySettingsControllerBase
 | 
				
			|||||||
  with RepositoryService with AccountService with WebHookService
 | 
					  with RepositoryService with AccountService with WebHookService
 | 
				
			||||||
  with OwnerAuthenticator with UsersAuthenticator
 | 
					  with OwnerAuthenticator with UsersAuthenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
 | 
					trait RepositorySettingsControllerBase extends ControllerBase {
 | 
				
			||||||
  self: RepositoryService with AccountService with WebHookService
 | 
					  self: RepositoryService with AccountService with WebHookService
 | 
				
			||||||
    with OwnerAuthenticator with UsersAuthenticator =>
 | 
					    with OwnerAuthenticator with UsersAuthenticator =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // for repository options
 | 
					  // for repository options
 | 
				
			||||||
  case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
 | 
					  case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  val optionsForm = mapping(
 | 
					  val optionsForm = mapping(
 | 
				
			||||||
 | 
					    "repositoryName" -> trim(label("Description"    , text(required, maxlength(40), identifier, renameRepositoryName))),
 | 
				
			||||||
    "description"    -> trim(label("Description"    , optional(text()))),
 | 
					    "description"    -> trim(label("Description"    , optional(text()))),
 | 
				
			||||||
    "defaultBranch"  -> trim(label("Default Branch" , text(required, maxlength(100)))),
 | 
					    "defaultBranch"  -> trim(label("Default Branch" , text(required, maxlength(100)))),
 | 
				
			||||||
    "isPrivate"      -> trim(label("Repository Type", boolean()))
 | 
					    "isPrivate"      -> trim(label("Repository Type", boolean()))
 | 
				
			||||||
@@ -42,6 +46,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
    "url" -> trim(label("url", text(required, webHook)))
 | 
					    "url" -> trim(label("url", text(required, webHook)))
 | 
				
			||||||
  )(WebHookForm.apply)
 | 
					  )(WebHookForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // for transfer ownership
 | 
				
			||||||
 | 
					  case class TransferOwnerShipForm(newOwner: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val transferForm = mapping(
 | 
				
			||||||
 | 
					    "newOwner" -> trim(label("New owner", text(required, transferUser)))
 | 
				
			||||||
 | 
					  )(TransferOwnerShipForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Redirect to the Options page.
 | 
					   * Redirect to the Options page.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -64,13 +75,26 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
      repository.owner,
 | 
					      repository.owner,
 | 
				
			||||||
      repository.name,
 | 
					      repository.name,
 | 
				
			||||||
      form.description,
 | 
					      form.description,
 | 
				
			||||||
      form.defaultBranch,
 | 
					      if(repository.branchList.isEmpty) "master" else form.defaultBranch,
 | 
				
			||||||
      repository.repository.parentUserName.map { _ =>
 | 
					      repository.repository.parentUserName.map { _ =>
 | 
				
			||||||
        repository.repository.isPrivate
 | 
					        repository.repository.isPrivate
 | 
				
			||||||
      } getOrElse form.isPrivate
 | 
					      } getOrElse form.isPrivate
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    // Change repository name
 | 
				
			||||||
 | 
					    if(repository.name != form.repositoryName){
 | 
				
			||||||
 | 
					      // Update database
 | 
				
			||||||
 | 
					      renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
 | 
				
			||||||
 | 
					      // Move git repository
 | 
				
			||||||
 | 
					      defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
 | 
				
			||||||
 | 
					        FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Move wiki repository
 | 
				
			||||||
 | 
					      defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
 | 
				
			||||||
 | 
					        FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    flash += "info" -> "Repository settings has been updated."
 | 
					    flash += "info" -> "Repository settings has been updated."
 | 
				
			||||||
    redirect(s"/${repository.owner}/${repository.name}/settings/options")
 | 
					    redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -137,15 +161,13 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
        .setMaxCount(3)
 | 
					        .setMaxCount(3)
 | 
				
			||||||
        .call.iterator.asScala.map(new CommitInfo(_))
 | 
					        .call.iterator.asScala.map(new CommitInfo(_))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      val webHookURLs = getWebHookURLs(repository.owner, repository.name)
 | 
					      getWebHookURLs(repository.owner, repository.name) match {
 | 
				
			||||||
      if(webHookURLs.nonEmpty){
 | 
					        case webHookURLs if(webHookURLs.nonEmpty) =>
 | 
				
			||||||
 | 
					          for(ownerAccount <- getAccountByUserName(repository.owner)){
 | 
				
			||||||
            callWebHook(repository.owner, repository.name, webHookURLs,
 | 
					            callWebHook(repository.owner, repository.name, webHookURLs,
 | 
				
			||||||
          WebHookPayload(
 | 
					              WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
 | 
				
			||||||
            git,
 | 
					          }
 | 
				
			||||||
            "refs/heads/" + repository.repository.defaultBranch,
 | 
					        case _ =>
 | 
				
			||||||
            repository,
 | 
					 | 
				
			||||||
            commits.toList,
 | 
					 | 
				
			||||||
            getAccountByUserName(repository.owner).get))
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      flash += "info" -> "Test payload deployed!"
 | 
					      flash += "info" -> "Test payload deployed!"
 | 
				
			||||||
@@ -154,22 +176,45 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Display the delete repository page.
 | 
					   * Display the danger zone.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  get("/:owner/:repository/settings/delete")(ownerOnly {
 | 
					  get("/:owner/:repository/settings/danger")(ownerOnly {
 | 
				
			||||||
    settings.html.delete(_)
 | 
					    settings.html.danger(_)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Transfer repository ownership.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) =>
 | 
				
			||||||
 | 
					    // Change repository owner
 | 
				
			||||||
 | 
					    if(repository.owner != form.newOwner){
 | 
				
			||||||
 | 
					      LockUtil.lock(s"${repository.owner}/${repository.name}"){
 | 
				
			||||||
 | 
					        // Update database
 | 
				
			||||||
 | 
					        renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
 | 
				
			||||||
 | 
					        // Move git repository
 | 
				
			||||||
 | 
					        defining(getRepositoryDir(repository.owner, repository.name)){ dir =>
 | 
				
			||||||
 | 
					          FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Move wiki repository
 | 
				
			||||||
 | 
					        defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir =>
 | 
				
			||||||
 | 
					          FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    redirect(s"/${form.newOwner}/${repository.name}")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Delete the repository.
 | 
					   * Delete the repository.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
 | 
					  post("/:owner/:repository/settings/delete")(ownerOnly { repository =>
 | 
				
			||||||
 | 
					    LockUtil.lock(s"${repository.owner}/${repository.name}"){
 | 
				
			||||||
      deleteRepository(repository.owner, repository.name)
 | 
					      deleteRepository(repository.owner, repository.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
 | 
					      FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
 | 
				
			||||||
      FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
 | 
					      FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
 | 
				
			||||||
      FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
 | 
					      FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    redirect(s"/${repository.owner}")
 | 
					    redirect(s"/${repository.owner}")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -177,7 +222,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
   * Provides duplication check for web hook url.
 | 
					   * Provides duplication check for web hook url.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def webHook: Constraint = new Constraint(){
 | 
					  private def webHook: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
 | 
					      getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -185,7 +230,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
   * Provides Constraint to validate the collaborator name.
 | 
					   * Provides Constraint to validate the collaborator name.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def collaborator: Constraint = new Constraint(){
 | 
					  private def collaborator: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      getAccountByUserName(value) match {
 | 
					      getAccountByUserName(value) match {
 | 
				
			||||||
        case None => Some("User does not exist.")
 | 
					        case None => Some("User does not exist.")
 | 
				
			||||||
        case Some(x) if(x.isGroupAccount)
 | 
					        case Some(x) if(x.isGroupAccount)
 | 
				
			||||||
@@ -196,4 +241,32 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Duplicate check for the rename repository name.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def renameRepositoryName: Constraint = new Constraint(){
 | 
				
			||||||
 | 
					    override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
 | 
				
			||||||
 | 
					      params.get("repository").filter(_ != value).flatMap { _ =>
 | 
				
			||||||
 | 
					        params.get("owner").flatMap { userName =>
 | 
				
			||||||
 | 
					          getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Provides Constraint to validate the repository transfer user.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def transferUser: Constraint = new Constraint(){
 | 
				
			||||||
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
 | 
					      getAccountByUserName(value) match {
 | 
				
			||||||
 | 
					        case None    => Some("User does not exist.")
 | 
				
			||||||
 | 
					        case Some(x) => if(x.userName == params("owner")){
 | 
				
			||||||
 | 
					          Some("This is current repository owner.")
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          params.get("repository").flatMap { repositoryName =>
 | 
				
			||||||
 | 
					            getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,26 +1,74 @@
 | 
				
			|||||||
package app
 | 
					package app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import _root_.util.JGitUtil.CommitInfo
 | 
				
			||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import _root_.util.ControlUtil._
 | 
				
			||||||
import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
 | 
					import _root_.util._
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import org.scalatra._
 | 
					import org.scalatra._
 | 
				
			||||||
import java.io.File
 | 
					import java.io.File
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.{ArchiveCommand, Git}
 | 
				
			||||||
 | 
					import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
 | 
				
			||||||
import org.eclipse.jgit.lib._
 | 
					import org.eclipse.jgit.lib._
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
import org.eclipse.jgit.treewalk._
 | 
					import org.eclipse.jgit.treewalk._
 | 
				
			||||||
import java.util.zip.{ZipEntry, ZipOutputStream}
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
 | 
					import org.eclipse.jgit.dircache.DirCache
 | 
				
			||||||
 | 
					import org.eclipse.jgit.revwalk.RevCommit
 | 
				
			||||||
 | 
					import service.WebHookService.WebHookPayload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
 | 
					class RepositoryViewerController extends RepositoryViewerControllerBase
 | 
				
			||||||
  with RepositoryService with AccountService with ReferrerAuthenticator
 | 
					  with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
 | 
				
			||||||
 | 
					  with ReferrerAuthenticator with CollaboratorsAuthenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The repository viewer.
 | 
					 * The repository viewer.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait RepositoryViewerControllerBase extends ControllerBase {
 | 
					trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			||||||
  self: RepositoryService with AccountService with ReferrerAuthenticator =>
 | 
					  self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
 | 
				
			||||||
 | 
					    with ReferrerAuthenticator with CollaboratorsAuthenticator =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ArchiveCommand.registerFormat("zip", new ZipFormat)
 | 
				
			||||||
 | 
					  ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class EditorForm(
 | 
				
			||||||
 | 
					    branch: String,
 | 
				
			||||||
 | 
					    path: String,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					    message: Option[String],
 | 
				
			||||||
 | 
					    charset: String,
 | 
				
			||||||
 | 
					    lineSeparator: String,
 | 
				
			||||||
 | 
					    newFileName: String,
 | 
				
			||||||
 | 
					    oldFileName: Option[String]
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class DeleteForm(
 | 
				
			||||||
 | 
					    branch: String,
 | 
				
			||||||
 | 
					    path: String,
 | 
				
			||||||
 | 
					    message: Option[String],
 | 
				
			||||||
 | 
					    fileName: String
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val editorForm = mapping(
 | 
				
			||||||
 | 
					    "branch"        -> trim(label("Branch", text(required))),
 | 
				
			||||||
 | 
					    "path"          -> trim(label("Path", text())),
 | 
				
			||||||
 | 
					    "content"       -> trim(label("Content", text(required))),
 | 
				
			||||||
 | 
					    "message"       -> trim(label("Message", optional(text()))),
 | 
				
			||||||
 | 
					    "charset"       -> trim(label("Charset", text(required))),
 | 
				
			||||||
 | 
					    "lineSeparator" -> trim(label("Line Separator", text(required))),
 | 
				
			||||||
 | 
					    "newFileName"   -> trim(label("Filename", text(required))),
 | 
				
			||||||
 | 
					    "oldFileName"   -> trim(label("Old filename", optional(text())))
 | 
				
			||||||
 | 
					  )(EditorForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val deleteForm = mapping(
 | 
				
			||||||
 | 
					    "branch"   -> trim(label("Branch", text(required))),
 | 
				
			||||||
 | 
					    "path"     -> trim(label("Path", text())),
 | 
				
			||||||
 | 
					    "message"  -> trim(label("Message", optional(text()))),
 | 
				
			||||||
 | 
					    "fileName" -> trim(label("Filename", text(required)))
 | 
				
			||||||
 | 
					  )(DeleteForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns converted HTML from Markdown for preview.
 | 
					   * Returns converted HTML from Markdown for preview.
 | 
				
			||||||
@@ -63,13 +111,77 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
        case Right((logs, hasNext)) =>
 | 
					        case Right((logs, hasNext)) =>
 | 
				
			||||||
          repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
 | 
					          repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
 | 
				
			||||||
            logs.splitWith{ (commit1, commit2) =>
 | 
					            logs.splitWith{ (commit1, commit2) =>
 | 
				
			||||||
              view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
 | 
					              view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
 | 
				
			||||||
            }, page, hasNext)
 | 
					            }, page, hasNext)
 | 
				
			||||||
        case Left(_) => NotFound
 | 
					        case Left(_) => NotFound
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
 | 
				
			||||||
 | 
					    val (branch, path) = splitPath(repository, multiParams("splat").head)
 | 
				
			||||||
 | 
					    repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
 | 
				
			||||||
 | 
					      None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
 | 
				
			||||||
 | 
					    val (branch, path) = splitPath(repository, multiParams("splat").head)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					      val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      getPathObjectId(git, path, revCommit).map { objectId =>
 | 
				
			||||||
 | 
					        val paths = path.split("/")
 | 
				
			||||||
 | 
					        repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
 | 
				
			||||||
 | 
					          JGitUtil.getContentInfo(git, path, objectId))
 | 
				
			||||||
 | 
					      } getOrElse NotFound
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
 | 
				
			||||||
 | 
					    val (branch, path) = splitPath(repository, multiParams("splat").head)
 | 
				
			||||||
 | 
					    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					      val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      getPathObjectId(git, path, revCommit).map { objectId =>
 | 
				
			||||||
 | 
					        val paths = path.split("/")
 | 
				
			||||||
 | 
					        repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
 | 
				
			||||||
 | 
					          JGitUtil.getContentInfo(git, path, objectId))
 | 
				
			||||||
 | 
					      } getOrElse NotFound
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
 | 
				
			||||||
 | 
					    commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
 | 
				
			||||||
 | 
					      StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
 | 
				
			||||||
 | 
					      form.message.getOrElse(s"Create ${form.newFileName}"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
 | 
				
			||||||
 | 
					      if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
 | 
				
			||||||
 | 
					    }")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
 | 
				
			||||||
 | 
					    commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
 | 
				
			||||||
 | 
					      StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
 | 
				
			||||||
 | 
					      if(form.oldFileName.exists(_ == form.newFileName)){
 | 
				
			||||||
 | 
					        form.message.getOrElse(s"Update ${form.newFileName}")
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
 | 
				
			||||||
 | 
					      if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
 | 
				
			||||||
 | 
					    }")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
 | 
				
			||||||
 | 
					    commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
 | 
				
			||||||
 | 
					      form.message.getOrElse(s"Delete ${form.fileName}"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Displays the file content of the specified branch or commit.
 | 
					   * Displays the file content of the specified branch or commit.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -79,46 +191,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
					    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
      val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
 | 
					      val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
 | 
				
			||||||
 | 
					      val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
 | 
				
			||||||
      @scala.annotation.tailrec
 | 
					      getPathObjectId(git, path, revCommit).map { objectId =>
 | 
				
			||||||
      def getPathObjectId(path: String, walk: TreeWalk): ObjectId = walk.next match {
 | 
					 | 
				
			||||||
        case true if(walk.getPathString == path) => walk.getObjectId(0)
 | 
					 | 
				
			||||||
        case true => getPathObjectId(path, walk)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
 | 
					 | 
				
			||||||
        treeWalk.addTree(revCommit.getTree)
 | 
					 | 
				
			||||||
        treeWalk.setRecursive(true)
 | 
					 | 
				
			||||||
        getPathObjectId(path, treeWalk)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if(raw){
 | 
					        if(raw){
 | 
				
			||||||
          // Download
 | 
					          // Download
 | 
				
			||||||
        defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
 | 
					          defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
 | 
				
			||||||
            contentType = FileUtil.getContentType(path, bytes)
 | 
					            contentType = FileUtil.getContentType(path, bytes)
 | 
				
			||||||
            bytes
 | 
					            bytes
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
        // Viewer
 | 
					          repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
 | 
				
			||||||
        val large  = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
 | 
					            new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
				
			||||||
        val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
 | 
					 | 
				
			||||||
        val bytes  = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val content = if(viewer == "other"){
 | 
					 | 
				
			||||||
          if(bytes.isDefined && FileUtil.isText(bytes.get)){
 | 
					 | 
				
			||||||
            // text
 | 
					 | 
				
			||||||
            JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            // binary
 | 
					 | 
				
			||||||
            JGitUtil.ContentInfo("binary", None)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          // image or large
 | 
					 | 
				
			||||||
          JGitUtil.ContentInfo(viewer, None)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit))
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      } getOrElse NotFound
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -150,10 +235,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
        val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
 | 
					        val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
 | 
				
			||||||
        (branchName, revCommit.getCommitterIdent.getWhen)
 | 
					        (branchName, revCommit.getCommitterIdent.getWhen)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      repo.html.branches(branchInfo, repository)
 | 
					      repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Deletes branch.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
 | 
				
			||||||
 | 
					    val branchName = multiParams("splat").head
 | 
				
			||||||
 | 
					    val userName   = context.loginAccount.get.userName
 | 
				
			||||||
 | 
					    if(repository.repository.defaultBranch != branchName){
 | 
				
			||||||
 | 
					      using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					        git.branchDelete().setForce(true).setBranchNames(branchName).call()
 | 
				
			||||||
 | 
					        recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    redirect(s"/${repository.owner}/${repository.name}/branches")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Displays tags.
 | 
					   * Displays tags.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -164,49 +264,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Download repository contents as an archive.
 | 
					   * Download repository contents as an archive.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  get("/:owner/:repository/archive/:name")(referrersOnly { repository =>
 | 
					  get("/:owner/:repository/archive/*")(referrersOnly { repository =>
 | 
				
			||||||
    val name = params("name")
 | 
					    multiParams("splat").head match {
 | 
				
			||||||
 | 
					      case name if name.endsWith(".zip") =>
 | 
				
			||||||
    if(name.endsWith(".zip")){
 | 
					        archiveRepository(name, ".zip", repository)
 | 
				
			||||||
      val revision = name.replaceFirst("\\.zip$", "")
 | 
					      case name if name.endsWith(".tar.gz") =>
 | 
				
			||||||
      val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
 | 
					        archiveRepository(name, ".tar.gz", repository)
 | 
				
			||||||
      if(workDir.exists){
 | 
					      case _ => BadRequest
 | 
				
			||||||
        FileUtils.deleteDirectory(workDir)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      workDir.mkdirs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
					 | 
				
			||||||
        val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
 | 
					 | 
				
			||||||
        using(new TreeWalk(git.getRepository)){ walk =>
 | 
					 | 
				
			||||||
          val reader   = walk.getObjectReader
 | 
					 | 
				
			||||||
          val objectId = new MutableObjectId
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out =>
 | 
					 | 
				
			||||||
            walk.addTree(revCommit.getTree)
 | 
					 | 
				
			||||||
            walk.setRecursive(true)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            while(walk.next){
 | 
					 | 
				
			||||||
              val name = walk.getPathString
 | 
					 | 
				
			||||||
              val mode = walk.getFileMode(0)
 | 
					 | 
				
			||||||
              if(mode != FileMode.TREE){
 | 
					 | 
				
			||||||
                walk.getObjectId(objectId, 0)
 | 
					 | 
				
			||||||
                val entry = new ZipEntry(name)
 | 
					 | 
				
			||||||
                val loader = reader.open(objectId)
 | 
					 | 
				
			||||||
                entry.setSize(loader.getSize)
 | 
					 | 
				
			||||||
                out.putNextEntry(entry)
 | 
					 | 
				
			||||||
                loader.copyTo(out)
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      contentType = "application/octet-stream"
 | 
					 | 
				
			||||||
      zipFile
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      BadRequest
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -215,7 +279,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
      getRepository(
 | 
					      getRepository(
 | 
				
			||||||
        repository.repository.originUserName.getOrElse(repository.owner),
 | 
					        repository.repository.originUserName.getOrElse(repository.owner),
 | 
				
			||||||
        repository.repository.originRepositoryName.getOrElse(repository.name),
 | 
					        repository.repository.originRepositoryName.getOrElse(repository.name),
 | 
				
			||||||
        baseUrl),
 | 
					        context.baseUrl),
 | 
				
			||||||
      getForkedRepositories(
 | 
					      getForkedRepositories(
 | 
				
			||||||
        repository.repository.originUserName.getOrElse(repository.owner),
 | 
					        repository.repository.originUserName.getOrElse(repository.owner),
 | 
				
			||||||
        repository.repository.originRepositoryName.getOrElse(repository.name)),
 | 
					        repository.repository.originRepositoryName.getOrElse(repository.name)),
 | 
				
			||||||
@@ -227,11 +291,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
      case branch if(path == branch || path.startsWith(branch + "/")) => branch
 | 
					      case branch if(path == branch || path.startsWith(branch + "/")) => branch
 | 
				
			||||||
    } orElse repository.tags.collectFirst {
 | 
					    } orElse repository.tags.collectFirst {
 | 
				
			||||||
      case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
 | 
					      case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
 | 
				
			||||||
    } orElse Some(path.split("/")(0)) get
 | 
					    } getOrElse path.split("/")(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    (id, path.substring(id.length).replaceFirst("^/", ""))
 | 
					    (id, path.substring(id.length).stripPrefix("/"))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Provides HTML of the file list.
 | 
					   * Provides HTML of the file list.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
@@ -242,28 +309,135 @@ trait RepositoryViewerControllerBase extends ControllerBase {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
 | 
					  private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
 | 
				
			||||||
    if(repository.commitCount == 0){
 | 
					    if(repository.commitCount == 0){
 | 
				
			||||||
      repo.html.guide(repository)
 | 
					      repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      using(Git.open(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
 | 
					        // get specified commit
 | 
				
			||||||
        JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
 | 
					        JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
 | 
				
			||||||
          defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
 | 
					          defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
 | 
				
			||||||
 | 
					            val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
 | 
				
			||||||
            // get files
 | 
					            // get files
 | 
				
			||||||
            val files = JGitUtil.getFileList(git, revision, path)
 | 
					            val files = JGitUtil.getFileList(git, revision, path)
 | 
				
			||||||
            // process README.md
 | 
					            val parentPath = if (path == ".") Nil else path.split("/").toList
 | 
				
			||||||
            val readme = files.find(_.name == "README.md").map { file =>
 | 
					            // process README.md or README.markdown
 | 
				
			||||||
              StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
 | 
					            val readme = files.find { file =>
 | 
				
			||||||
 | 
					              readmeFiles.contains(file.name.toLowerCase)
 | 
				
			||||||
 | 
					            }.map { file =>
 | 
				
			||||||
 | 
					              val path = (file.name :: parentPath.reverse).reverse
 | 
				
			||||||
 | 
					              path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
 | 
				
			||||||
 | 
					                Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            repo.html.files(revision, repository,
 | 
					            repo.html.files(revision, repository,
 | 
				
			||||||
              if(path == ".") Nil else path.split("/").toList, // current path
 | 
					              if(path == ".") Nil else path.split("/").toList, // current path
 | 
				
			||||||
              new JGitUtil.CommitInfo(revCommit), // latest commit
 | 
					              new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
 | 
				
			||||||
              files, readme)
 | 
					              files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } getOrElse NotFound
 | 
					        } getOrElse NotFound
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def commitFile(repository: service.RepositoryService.RepositoryInfo,
 | 
				
			||||||
 | 
					                         branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
 | 
				
			||||||
 | 
					                         content: String, charset: String, message: String) = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
 | 
				
			||||||
 | 
					    val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LockUtil.lock(s"${repository.owner}/${repository.name}"){
 | 
				
			||||||
 | 
					      using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					        val loginAccount = context.loginAccount.get
 | 
				
			||||||
 | 
					        val builder  = DirCache.newInCore.builder()
 | 
				
			||||||
 | 
					        val inserter = git.getRepository.newObjectInserter()
 | 
				
			||||||
 | 
					        val headName = s"refs/heads/${branch}"
 | 
				
			||||||
 | 
					        val headTip  = git.getRepository.resolve(headName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        JGitUtil.processTree(git, headTip){ (path, tree) =>
 | 
				
			||||||
 | 
					          if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
 | 
				
			||||||
 | 
					            builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        newPath.foreach { newPath =>
 | 
				
			||||||
 | 
					          builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
 | 
				
			||||||
 | 
					            inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        builder.finish()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
 | 
				
			||||||
 | 
					          headName, loginAccount.fullName, loginAccount.mailAddress, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inserter.flush()
 | 
				
			||||||
 | 
					        inserter.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // update refs
 | 
				
			||||||
 | 
					        val refUpdate = git.getRepository.updateRef(headName)
 | 
				
			||||||
 | 
					        refUpdate.setNewObjectId(commitId)
 | 
				
			||||||
 | 
					        refUpdate.setForceUpdate(false)
 | 
				
			||||||
 | 
					        refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
 | 
				
			||||||
 | 
					        //refUpdate.setRefLogMessage("merged", true)
 | 
				
			||||||
 | 
					        refUpdate.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // record activity
 | 
				
			||||||
 | 
					        recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
 | 
				
			||||||
 | 
					          List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // close issue by commit message
 | 
				
			||||||
 | 
					        closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // call web hook
 | 
				
			||||||
 | 
					        val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
 | 
				
			||||||
 | 
					        getWebHookURLs(repository.owner, repository.name) match {
 | 
				
			||||||
 | 
					          case webHookURLs if(webHookURLs.nonEmpty) =>
 | 
				
			||||||
 | 
					            for(ownerAccount <- getAccountByUserName(repository.owner)){
 | 
				
			||||||
 | 
					              callWebHook(repository.owner, repository.name, webHookURLs,
 | 
				
			||||||
 | 
					                WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          case _ =>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
 | 
				
			||||||
 | 
					    @scala.annotation.tailrec
 | 
				
			||||||
 | 
					    def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
 | 
				
			||||||
 | 
					      case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
 | 
				
			||||||
 | 
					      case true  => _getPathObjectId(path, walk)
 | 
				
			||||||
 | 
					      case false => None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    using(new TreeWalk(git.getRepository)){ treeWalk =>
 | 
				
			||||||
 | 
					      treeWalk.addTree(revCommit.getTree)
 | 
				
			||||||
 | 
					      treeWalk.setRecursive(true)
 | 
				
			||||||
 | 
					      _getPathObjectId(path, treeWalk)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
 | 
				
			||||||
 | 
					    val revision = name.stripSuffix(suffix)
 | 
				
			||||||
 | 
					    val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
 | 
				
			||||||
 | 
					    if(workDir.exists) {
 | 
				
			||||||
 | 
					      FileUtils.deleteDirectory(workDir)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    workDir.mkdirs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val file = new File(workDir, repository.name + "-" +
 | 
				
			||||||
 | 
					      (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
 | 
					      val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
 | 
				
			||||||
 | 
					      using(new java.io.FileOutputStream(file))  { out =>
 | 
				
			||||||
 | 
					        git.archive
 | 
				
			||||||
 | 
					           .setFormat(suffix.tail)
 | 
				
			||||||
 | 
					           .setTree(revCommit.getTree)
 | 
				
			||||||
 | 
					           .setOutputStream(out)
 | 
				
			||||||
 | 
					           .call()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      contentType = "application/octet-stream"
 | 
				
			||||||
 | 
					      response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
 | 
				
			||||||
 | 
					      file
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,17 +2,15 @@ package app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import util._
 | 
					import util._
 | 
				
			||||||
import ControlUtil._
 | 
					import ControlUtil._
 | 
				
			||||||
 | 
					import Implicits._
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchController extends SearchControllerBase
 | 
					class SearchController extends SearchControllerBase
 | 
				
			||||||
with RepositoryService with AccountService with SystemSettingsService with ActivityService
 | 
					  with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
 | 
				
			||||||
with RepositorySearchService with IssuesService
 | 
					 | 
				
			||||||
with ReferrerAuthenticator
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait SearchControllerBase extends ControllerBase { self: RepositoryService
 | 
					trait SearchControllerBase extends ControllerBase { self: RepositoryService
 | 
				
			||||||
  with SystemSettingsService with ActivityService with RepositorySearchService
 | 
					  with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
 | 
				
			||||||
  with ReferrerAuthenticator =>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val searchForm = mapping(
 | 
					  val searchForm = mapping(
 | 
				
			||||||
    "query"      -> trim(text(required)),
 | 
					    "query"      -> trim(text(required)),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,58 +0,0 @@
 | 
				
			|||||||
package app
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import service._
 | 
					 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					 | 
				
			||||||
import util.Implicits._
 | 
					 | 
				
			||||||
import util.StringUtil._
 | 
					 | 
				
			||||||
import util.Keys
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
trait SignInControllerBase extends ControllerBase { self: SystemSettingsService with AccountService =>
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  case class SignInForm(userName: String, password: String)
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  val form = mapping(
 | 
					 | 
				
			||||||
    "userName" -> trim(label("Username", text(required))),
 | 
					 | 
				
			||||||
    "password" -> trim(label("Password", text(required)))
 | 
					 | 
				
			||||||
  )(SignInForm.apply)
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  get("/signin"){
 | 
					 | 
				
			||||||
    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 =>
 | 
					 | 
				
			||||||
    authenticate(loadSystemSettings(), form.userName, form.password) match {
 | 
					 | 
				
			||||||
      case Some(account) => signin(account)
 | 
					 | 
				
			||||||
      case None          => redirect("/signin")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get("/signout"){
 | 
					 | 
				
			||||||
    session.invalidate
 | 
					 | 
				
			||||||
    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(urlEncode(redirectUrl).replaceAll("%2F", "/"))
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }.getOrElse {
 | 
					 | 
				
			||||||
      redirect("/")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -3,19 +3,29 @@ package app
 | 
				
			|||||||
import service.{AccountService, SystemSettingsService}
 | 
					import service.{AccountService, SystemSettingsService}
 | 
				
			||||||
import SystemSettingsService._
 | 
					import SystemSettingsService._
 | 
				
			||||||
import util.AdminAuthenticator
 | 
					import util.AdminAuthenticator
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import util.ControlUtil._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.scalatra.FlashMapSupport
 | 
					import ssh.SshServer
 | 
				
			||||||
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					import java.io.FileInputStream
 | 
				
			||||||
 | 
					import plugin.{Plugin, PluginSystem}
 | 
				
			||||||
 | 
					import org.scalatra.Ok
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SystemSettingsController extends SystemSettingsControllerBase
 | 
					class SystemSettingsController extends SystemSettingsControllerBase
 | 
				
			||||||
  with SystemSettingsService with AccountService with AdminAuthenticator
 | 
					  with AccountService with AdminAuthenticator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
 | 
					trait SystemSettingsControllerBase extends ControllerBase {
 | 
				
			||||||
  self: SystemSettingsService with AccountService with AdminAuthenticator =>
 | 
					  self: AccountService with AdminAuthenticator =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val form = mapping(
 | 
					  private val form = mapping(
 | 
				
			||||||
 | 
					    "baseUrl"                  -> trim(label("Base URL", optional(text()))),
 | 
				
			||||||
    "allowAccountRegistration" -> trim(label("Account registration", boolean())),
 | 
					    "allowAccountRegistration" -> trim(label("Account registration", boolean())),
 | 
				
			||||||
    "gravatar"                 -> trim(label("Gravatar", boolean())),
 | 
					    "gravatar"                 -> trim(label("Gravatar", boolean())),
 | 
				
			||||||
    "notification"             -> trim(label("Notification", boolean())),
 | 
					    "notification"             -> trim(label("Notification", boolean())),
 | 
				
			||||||
 | 
					    "ssh"                      -> trim(label("SSH access", boolean())),
 | 
				
			||||||
 | 
					    "sshPort"                  -> trim(label("SSH port", optional(number()))),
 | 
				
			||||||
    "smtp"                     -> optionalIfNotChecked("notification", mapping(
 | 
					    "smtp"                     -> optionalIfNotChecked("notification", mapping(
 | 
				
			||||||
        "host"                     -> trim(label("SMTP Host", text(required))),
 | 
					        "host"                     -> trim(label("SMTP Host", text(required))),
 | 
				
			||||||
        "port"                     -> trim(label("SMTP Port", optional(number()))),
 | 
					        "port"                     -> trim(label("SMTP Port", optional(number()))),
 | 
				
			||||||
@@ -33,19 +43,145 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
        "bindPassword"             -> trim(label("Bind Password", optional(text()))),
 | 
					        "bindPassword"             -> trim(label("Bind Password", optional(text()))),
 | 
				
			||||||
        "baseDN"                   -> trim(label("Base DN", text(required))),
 | 
					        "baseDN"                   -> trim(label("Base DN", text(required))),
 | 
				
			||||||
        "userNameAttribute"        -> trim(label("User name attribute", text(required))),
 | 
					        "userNameAttribute"        -> trim(label("User name attribute", text(required))),
 | 
				
			||||||
        "mailAttribute"            -> trim(label("Mail address attribute", text(required)))
 | 
					        "additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
 | 
				
			||||||
 | 
					        "fullNameAttribute"        -> trim(label("Full name attribute", optional(text()))),
 | 
				
			||||||
 | 
					        "mailAttribute"            -> trim(label("Mail address attribute", optional(text()))),
 | 
				
			||||||
 | 
					        "tls"                      -> trim(label("Enable TLS", optional(boolean()))),
 | 
				
			||||||
 | 
					        "keystore"                 -> trim(label("Keystore", optional(text())))
 | 
				
			||||||
    )(Ldap.apply))
 | 
					    )(Ldap.apply))
 | 
				
			||||||
  )(SystemSettings.apply)
 | 
					  )(SystemSettings.apply).verifying { settings =>
 | 
				
			||||||
 | 
					    if(settings.ssh && settings.baseUrl.isEmpty){
 | 
				
			||||||
 | 
					      Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
 | 
				
			||||||
 | 
					    } else Nil
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val pluginForm = mapping(
 | 
				
			||||||
 | 
					    "pluginId" -> list(trim(label("", text())))
 | 
				
			||||||
 | 
					  )(PluginForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class PluginForm(pluginIds: List[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/admin/system")(adminOnly {
 | 
					  get("/admin/system")(adminOnly {
 | 
				
			||||||
    admin.html.system(loadSystemSettings(), flash.get("info"))
 | 
					    admin.html.system(flash.get("info"))
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/admin/system", form)(adminOnly { form =>
 | 
					  post("/admin/system", form)(adminOnly { form =>
 | 
				
			||||||
    saveSystemSettings(form)
 | 
					    saveSystemSettings(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
 | 
				
			||||||
 | 
					      SshServer.stop()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
 | 
				
			||||||
 | 
					      SshServer.start(request.getServletContext,
 | 
				
			||||||
 | 
					        form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
 | 
				
			||||||
 | 
					        form.baseUrl.get)
 | 
				
			||||||
 | 
					    } else if(!form.ssh && SshServer.isActive){
 | 
				
			||||||
 | 
					      SshServer.stop()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    flash += "info" -> "System settings has been updated."
 | 
					    flash += "info" -> "System settings has been updated."
 | 
				
			||||||
    redirect("/admin/system")
 | 
					    redirect("/admin/system")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/admin/plugins")(adminOnly {
 | 
				
			||||||
 | 
					    val installedPlugins = plugin.PluginSystem.plugins
 | 
				
			||||||
 | 
					    val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
 | 
				
			||||||
 | 
					    admin.plugins.html.installed(installedPlugins, updatablePlugins)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
 | 
				
			||||||
 | 
					    deletePlugins(form.pluginIds)
 | 
				
			||||||
 | 
					    installPlugins(form.pluginIds)
 | 
				
			||||||
 | 
					    redirect("/admin/plugins")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
 | 
				
			||||||
 | 
					    deletePlugins(form.pluginIds)
 | 
				
			||||||
 | 
					    redirect("/admin/plugins")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/admin/plugins/available")(adminOnly {
 | 
				
			||||||
 | 
					    val installedPlugins = plugin.PluginSystem.plugins
 | 
				
			||||||
 | 
					    val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
 | 
				
			||||||
 | 
					    admin.plugins.html.available(availablePlugins)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
 | 
				
			||||||
 | 
					    installPlugins(form.pluginIds)
 | 
				
			||||||
 | 
					    redirect("/admin/plugins")
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get("/admin/plugins/console")(adminOnly {
 | 
				
			||||||
 | 
					    admin.plugins.html.console()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  post("/admin/plugins/console")(adminOnly {
 | 
				
			||||||
 | 
					    val script = request.getParameter("script")
 | 
				
			||||||
 | 
					    val result = plugin.ScalaPlugin.eval(script)
 | 
				
			||||||
 | 
					    Ok()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO Move these methods to PluginSystem or Service?
 | 
				
			||||||
 | 
					  private def deletePlugins(pluginIds: List[String]): Unit = {
 | 
				
			||||||
 | 
					    pluginIds.foreach { pluginId =>
 | 
				
			||||||
 | 
					      plugin.PluginSystem.uninstall(pluginId)
 | 
				
			||||||
 | 
					      val dir = new java.io.File(PluginHome, pluginId)
 | 
				
			||||||
 | 
					      if(dir.exists && dir.isDirectory){
 | 
				
			||||||
 | 
					        FileUtils.deleteQuietly(dir)
 | 
				
			||||||
 | 
					        PluginSystem.uninstall(pluginId)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def installPlugins(pluginIds: List[String]): Unit = {
 | 
				
			||||||
 | 
					    val dir = getPluginCacheDir()
 | 
				
			||||||
 | 
					    val installedPlugins = plugin.PluginSystem.plugins
 | 
				
			||||||
 | 
					    getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
 | 
				
			||||||
 | 
					      val pluginDir = new java.io.File(PluginHome, plugin.id)
 | 
				
			||||||
 | 
					      if(pluginDir.exists){
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(pluginDir)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
 | 
				
			||||||
 | 
					      PluginSystem.installPlugin(plugin.id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
 | 
				
			||||||
 | 
					    val repositoryRoot = getPluginCacheDir()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(repositoryRoot.exists && repositoryRoot.isDirectory){
 | 
				
			||||||
 | 
					      PluginSystem.repositories.flatMap { repo =>
 | 
				
			||||||
 | 
					        val repoDir = new java.io.File(repositoryRoot, repo.id)
 | 
				
			||||||
 | 
					        if(repoDir.exists && repoDir.isDirectory){
 | 
				
			||||||
 | 
					          repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
 | 
				
			||||||
 | 
					            val propertyFile = new java.io.File(plugin, "plugin.properties")
 | 
				
			||||||
 | 
					            val properties = new java.util.Properties()
 | 
				
			||||||
 | 
					            if(propertyFile.exists && propertyFile.isFile){
 | 
				
			||||||
 | 
					              using(new FileInputStream(propertyFile)){ in =>
 | 
				
			||||||
 | 
					                properties.load(in)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            SystemSettingsControllerBase.AvailablePlugin(
 | 
				
			||||||
 | 
					              repository  = repo.id,
 | 
				
			||||||
 | 
					              id          = properties.getProperty("id"),
 | 
				
			||||||
 | 
					              version     = properties.getProperty("version"),
 | 
				
			||||||
 | 
					              author      = properties.getProperty("author"),
 | 
				
			||||||
 | 
					              url         = properties.getProperty("url"),
 | 
				
			||||||
 | 
					              description = properties.getProperty("description"),
 | 
				
			||||||
 | 
					              status      = installedPlugins.find(_.id == properties.getProperty("id")) match {
 | 
				
			||||||
 | 
					                case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
 | 
				
			||||||
 | 
					                case Some(x) => "installed"
 | 
				
			||||||
 | 
					                case None    => "available"
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else Nil
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else Nil
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object SystemSettingsControllerBase {
 | 
				
			||||||
 | 
					  case class AvailablePlugin(repository: String, id: String, version: String,
 | 
				
			||||||
 | 
					                             author: String, url: String, description: String, status: String)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,11 @@ import service._
 | 
				
			|||||||
import util.AdminAuthenticator
 | 
					import util.AdminAuthenticator
 | 
				
			||||||
import util.StringUtil._
 | 
					import util.StringUtil._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserManagementController extends UserManagementControllerBase
 | 
					class UserManagementController extends UserManagementControllerBase
 | 
				
			||||||
  with AccountService with RepositoryService with AdminAuthenticator
 | 
					  with AccountService with RepositoryService with AdminAuthenticator
 | 
				
			||||||
@@ -17,57 +21,61 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
                         url: Option[String], fileId: Option[String])
 | 
					                         url: Option[String], fileId: Option[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class EditUserForm(userName: String, password: Option[String], fullName: String,
 | 
					  case class EditUserForm(userName: String, password: Option[String], fullName: String,
 | 
				
			||||||
                          mailAddress: String, isAdmin: Boolean,
 | 
					                          mailAddress: String, isAdmin: Boolean, url: Option[String],
 | 
				
			||||||
                          url: Option[String], fileId: Option[String], clearImage: Boolean)
 | 
					                          fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
 | 
					  case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
 | 
				
			||||||
                          memberNames: Option[String])
 | 
					                          members: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
 | 
					  case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
 | 
				
			||||||
                           memberNames: Option[String], clearImage: Boolean)
 | 
					                           members: String, clearImage: Boolean, isRemoved: Boolean)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val newUserForm = mapping(
 | 
					  val newUserForm = mapping(
 | 
				
			||||||
    "userName"    -> trim(label("Username"     , text(required, maxlength(100), identifier, uniqueUserName))),
 | 
					    "userName"    -> trim(label("Username"     ,text(required, maxlength(100), identifier, uniqueUserName))),
 | 
				
			||||||
    "password"    -> trim(label("Password"     , text(required, maxlength(20)))),
 | 
					    "password"    -> trim(label("Password"     ,text(required, maxlength(20)))),
 | 
				
			||||||
    "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))),
 | 
					    "fullName"    -> trim(label("Full Name"    ,text(required, maxlength(100)))),
 | 
				
			||||||
    "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
 | 
					    "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
 | 
				
			||||||
    "isAdmin"     -> trim(label("User Type"    , boolean())),
 | 
					    "isAdmin"     -> trim(label("User Type"    ,boolean())),
 | 
				
			||||||
    "url"         -> trim(label("URL"          , optional(text(maxlength(200))))),
 | 
					    "url"         -> trim(label("URL"          ,optional(text(maxlength(200))))),
 | 
				
			||||||
    "fileId"      -> trim(label("File ID"      , optional(text())))
 | 
					    "fileId"      -> trim(label("File ID"      ,optional(text())))
 | 
				
			||||||
  )(NewUserForm.apply)
 | 
					  )(NewUserForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val editUserForm = mapping(
 | 
					  val editUserForm = mapping(
 | 
				
			||||||
    "userName"    -> trim(label("Username"     , text(required, maxlength(100), identifier))),
 | 
					    "userName"    -> trim(label("Username"     ,text(required, maxlength(100), identifier))),
 | 
				
			||||||
    "password"    -> trim(label("Password"     , optional(text(maxlength(20))))),
 | 
					    "password"    -> trim(label("Password"     ,optional(text(maxlength(20))))),
 | 
				
			||||||
    "fullName"    -> trim(label("Full Name"    , text(required, maxlength(100)))),
 | 
					    "fullName"    -> trim(label("Full Name"    ,text(required, maxlength(100)))),
 | 
				
			||||||
    "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
 | 
					    "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
 | 
				
			||||||
    "isAdmin"     -> trim(label("User Type"    , boolean())),
 | 
					    "isAdmin"     -> trim(label("User Type"    ,boolean())),
 | 
				
			||||||
    "url"         -> trim(label("URL"          , optional(text(maxlength(200))))),
 | 
					    "url"         -> trim(label("URL"          ,optional(text(maxlength(200))))),
 | 
				
			||||||
    "fileId"      -> trim(label("File ID"      , optional(text()))),
 | 
					    "fileId"      -> trim(label("File ID"      ,optional(text()))),
 | 
				
			||||||
    "clearImage"  -> trim(label("Clear image"  , boolean()))
 | 
					    "clearImage"  -> trim(label("Clear image"  ,boolean())),
 | 
				
			||||||
 | 
					    "removed"     -> trim(label("Disable"      ,boolean()))
 | 
				
			||||||
  )(EditUserForm.apply)
 | 
					  )(EditUserForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val newGroupForm = mapping(
 | 
					  val newGroupForm = mapping(
 | 
				
			||||||
    "groupName"   -> trim(label("Group name"   , text(required, maxlength(100), identifier, uniqueUserName))),
 | 
					    "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
 | 
				
			||||||
    "url"         -> trim(label("URL"          , optional(text(maxlength(200))))),
 | 
					    "url"       -> trim(label("URL"        ,optional(text(maxlength(200))))),
 | 
				
			||||||
    "fileId"      -> trim(label("File ID"      , optional(text()))),
 | 
					    "fileId"    -> trim(label("File ID"    ,optional(text()))),
 | 
				
			||||||
    "memberNames" -> trim(label("Member Names" , optional(text())))
 | 
					    "members"   -> trim(label("Members"    ,text(required, members)))
 | 
				
			||||||
  )(NewGroupForm.apply)
 | 
					  )(NewGroupForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val editGroupForm = mapping(
 | 
					  val editGroupForm = mapping(
 | 
				
			||||||
    "groupName"   -> trim(label("Group name"   , text(required, maxlength(100), identifier))),
 | 
					    "groupName"  -> trim(label("Group name"  ,text(required, maxlength(100), identifier))),
 | 
				
			||||||
    "url"         -> trim(label("URL"          , optional(text(maxlength(200))))),
 | 
					    "url"        -> trim(label("URL"         ,optional(text(maxlength(200))))),
 | 
				
			||||||
    "fileId"      -> trim(label("File ID"      , optional(text()))),
 | 
					    "fileId"     -> trim(label("File ID"     ,optional(text()))),
 | 
				
			||||||
    "memberNames" -> trim(label("Member Names" , optional(text()))),
 | 
					    "members"    -> trim(label("Members"     ,text(required, members))),
 | 
				
			||||||
    "clearImage"  -> trim(label("Clear image"  , boolean()))
 | 
					    "clearImage" -> trim(label("Clear image" ,boolean())),
 | 
				
			||||||
 | 
					    "removed"    -> trim(label("Disable"     ,boolean()))
 | 
				
			||||||
  )(EditGroupForm.apply)
 | 
					  )(EditGroupForm.apply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/admin/users")(adminOnly {
 | 
					  get("/admin/users")(adminOnly {
 | 
				
			||||||
    val users = getAllUsers()
 | 
					    val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
 | 
				
			||||||
 | 
					    val users          = getAllUsers(includeRemoved)
 | 
				
			||||||
    val members        = users.collect { case account if(account.isGroupAccount) =>
 | 
					    val members        = users.collect { case account if(account.isGroupAccount) =>
 | 
				
			||||||
      account.userName -> getGroupMembers(account.userName)
 | 
					      account.userName -> getGroupMembers(account.userName).map(_.userName)
 | 
				
			||||||
    }.toMap
 | 
					    }.toMap
 | 
				
			||||||
    admin.users.html.list(users, members)
 | 
					
 | 
				
			||||||
 | 
					    admin.users.html.list(users, members, includeRemoved)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  get("/admin/users/_newuser")(adminOnly {
 | 
					  get("/admin/users/_newuser")(adminOnly {
 | 
				
			||||||
@@ -82,18 +90,32 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
  get("/admin/users/:userName/_edituser")(adminOnly {
 | 
					  get("/admin/users/:userName/_edituser")(adminOnly {
 | 
				
			||||||
    val userName = params("userName")
 | 
					    val userName = params("userName")
 | 
				
			||||||
    admin.users.html.user(getAccountByUserName(userName))
 | 
					    admin.users.html.user(getAccountByUserName(userName, true))
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
 | 
					  post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
 | 
				
			||||||
    val userName = params("userName")
 | 
					    val userName = params("userName")
 | 
				
			||||||
    getAccountByUserName(userName).map { account =>
 | 
					    getAccountByUserName(userName, true).map { account =>
 | 
				
			||||||
      updateAccount(getAccountByUserName(userName).get.copy(
 | 
					
 | 
				
			||||||
 | 
					      if(form.isRemoved){
 | 
				
			||||||
 | 
					        // Remove repositories
 | 
				
			||||||
 | 
					        getRepositoryNamesOfUser(userName).foreach { repositoryName =>
 | 
				
			||||||
 | 
					          deleteRepository(userName, repositoryName)
 | 
				
			||||||
 | 
					          FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					          FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					          FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
 | 
				
			||||||
 | 
					        removeUserRelatedData(userName)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      updateAccount(account.copy(
 | 
				
			||||||
        password     = form.password.map(sha1).getOrElse(account.password),
 | 
					        password     = form.password.map(sha1).getOrElse(account.password),
 | 
				
			||||||
        fullName     = form.fullName,
 | 
					        fullName     = form.fullName,
 | 
				
			||||||
        mailAddress  = form.mailAddress,
 | 
					        mailAddress  = form.mailAddress,
 | 
				
			||||||
        isAdmin      = form.isAdmin,
 | 
					        isAdmin      = form.isAdmin,
 | 
				
			||||||
        url          = form.url))
 | 
					        url          = form.url,
 | 
				
			||||||
 | 
					        isRemoved    = form.isRemoved))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      updateImage(userName, form.fileId, form.clearImage)
 | 
					      updateImage(userName, form.fileId, form.clearImage)
 | 
				
			||||||
      redirect("/admin/users")
 | 
					      redirect("/admin/users")
 | 
				
			||||||
@@ -107,29 +129,51 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
 | 
					  post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
 | 
				
			||||||
    createGroup(form.groupName, form.url)
 | 
					    createGroup(form.groupName, form.url)
 | 
				
			||||||
    updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
 | 
					    updateGroupMembers(form.groupName, form.members.split(",").map {
 | 
				
			||||||
 | 
					      _.split(":") match {
 | 
				
			||||||
 | 
					        case Array(userName, isManager) => (userName, isManager.toBoolean)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }.toList)
 | 
				
			||||||
    updateImage(form.groupName, form.fileId, false)
 | 
					    updateImage(form.groupName, form.fileId, false)
 | 
				
			||||||
    redirect("/admin/users")
 | 
					    redirect("/admin/users")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get("/admin/users/:groupName/_editgroup")(adminOnly {
 | 
					  get("/admin/users/:groupName/_editgroup")(adminOnly {
 | 
				
			||||||
    defining(params("groupName")){ groupName =>
 | 
					    defining(params("groupName")){ groupName =>
 | 
				
			||||||
      admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
 | 
					      admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
 | 
					  post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
 | 
				
			||||||
    defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
 | 
					    defining(params("groupName"), form.members.split(",").map {
 | 
				
			||||||
      getAccountByUserName(groupName).map { account =>
 | 
					      _.split(":") match {
 | 
				
			||||||
        updateGroup(groupName, form.url)
 | 
					        case Array(userName, isManager) => (userName, isManager.toBoolean)
 | 
				
			||||||
        updateGroupMembers(form.groupName, memberNames)
 | 
					      }
 | 
				
			||||||
 | 
					    }.toList){ case (groupName, members) =>
 | 
				
			||||||
 | 
					      getAccountByUserName(groupName, true).map { account =>
 | 
				
			||||||
 | 
					        updateGroup(groupName, form.url, form.isRemoved)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if(form.isRemoved){
 | 
				
			||||||
 | 
					          // Remove from GROUP_MEMBER
 | 
				
			||||||
 | 
					          updateGroupMembers(form.groupName, Nil)
 | 
				
			||||||
 | 
					          // Remove repositories
 | 
				
			||||||
 | 
					          getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
 | 
				
			||||||
 | 
					            deleteRepository(groupName, repositoryName)
 | 
				
			||||||
 | 
					            FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					            FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					            FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Update GROUP_MEMBER
 | 
				
			||||||
 | 
					          updateGroupMembers(form.groupName, members)
 | 
				
			||||||
 | 
					          // Update COLLABORATOR for group repositories
 | 
				
			||||||
          getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
 | 
					          getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
 | 
				
			||||||
            removeCollaborators(form.groupName, repositoryName)
 | 
					            removeCollaborators(form.groupName, repositoryName)
 | 
				
			||||||
          memberNames.foreach { userName =>
 | 
					            members.foreach { case (userName, isManager) =>
 | 
				
			||||||
              addCollaborator(form.groupName, repositoryName, userName)
 | 
					              addCollaborator(form.groupName, repositoryName, userName)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        updateImage(form.groupName, form.fileId, form.clearImage)
 | 
					        updateImage(form.groupName, form.fileId, form.clearImage)
 | 
				
			||||||
        redirect("/admin/users")
 | 
					        redirect("/admin/users")
 | 
				
			||||||
@@ -138,8 +182,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  post("/admin/users/_usercheck")(adminOnly {
 | 
					  private def members: Constraint = new Constraint(){
 | 
				
			||||||
    getAccountByUserName(params("userName")).isDefined
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] = {
 | 
				
			||||||
  })
 | 
					      if(value.split(",").exists {
 | 
				
			||||||
 | 
					        _.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
 | 
				
			||||||
 | 
					      }) None else Some("Must select one manager at least.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,19 +4,17 @@ import service._
 | 
				
			|||||||
import util._
 | 
					import util._
 | 
				
			||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import util.Implicits._
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import org.scalatra.FlashMapSupport
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
import service.WikiService.WikiPageInfo
 | 
					import java.util.ResourceBundle
 | 
				
			||||||
import scala.Some
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WikiController extends WikiControllerBase 
 | 
					class WikiController extends WikiControllerBase 
 | 
				
			||||||
  with WikiService with RepositoryService with AccountService with ActivityService
 | 
					  with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
 | 
				
			||||||
  with CollaboratorsAuthenticator with ReferrerAuthenticator
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
					trait WikiControllerBase extends ControllerBase {
 | 
				
			||||||
  self: WikiService with RepositoryService with ActivityService
 | 
					  self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
 | 
				
			||||||
    with CollaboratorsAuthenticator with ReferrerAuthenticator =>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
 | 
					  case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -38,7 +36,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
  get("/:owner/:repository/wiki")(referrersOnly { repository =>
 | 
					  get("/:owner/:repository/wiki")(referrersOnly { repository =>
 | 
				
			||||||
    getWikiPage(repository.owner, repository.name, "Home").map { page =>
 | 
					    getWikiPage(repository.owner, repository.name, "Home").map { page =>
 | 
				
			||||||
      wiki.html.page("Home", page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
					      wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name),
 | 
				
			||||||
 | 
					        repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
				
			||||||
    } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
 | 
					    } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -46,7 +45,8 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
    val pageName = StringUtil.urlDecode(params("page"))
 | 
					    val pageName = StringUtil.urlDecode(params("page"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getWikiPage(repository.owner, repository.name, pageName).map { page =>
 | 
					    getWikiPage(repository.owner, repository.name, pageName).map { page =>
 | 
				
			||||||
      wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
					      wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
 | 
				
			||||||
 | 
					        repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
 | 
				
			||||||
    } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
 | 
					    } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -66,7 +66,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
    val Array(from, to) = params("commitId").split("\\.\\.\\.")
 | 
					    val Array(from, to) = params("commitId").split("\\.\\.\\.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
 | 
					    using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
 | 
				
			||||||
      wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
 | 
					      wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
 | 
				
			||||||
        hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
 | 
					        hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
@@ -96,7 +96,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
    val Array(from, to) = params("commitId").split("\\.\\.\\.")
 | 
					    val Array(from, to) = params("commitId").split("\\.\\.\\.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
 | 
					    if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
 | 
				
			||||||
      redirect(s"/${repository.owner}/${repository.name}/wiki/}")
 | 
					      redirect(s"/${repository.owner}/${repository.name}/wiki/")
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      flash += "info" -> "This patch was not able to be reversed."
 | 
					      flash += "info" -> "This patch was not able to be reversed."
 | 
				
			||||||
      redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
 | 
					      redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
 | 
				
			||||||
@@ -170,12 +170,12 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def unique: Constraint = new Constraint(){
 | 
					  private def unique: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
 | 
					    override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
 | 
				
			||||||
      getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
 | 
					      getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def pagename: Constraint = new Constraint(){
 | 
					  private def pagename: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      if(value.exists("\\/:*?\"<>|".contains(_))){
 | 
					      if(value.exists("\\/:*?\"<>|".contains(_))){
 | 
				
			||||||
        Some(s"${name} contains invalid character.")
 | 
					        Some(s"${name} contains invalid character.")
 | 
				
			||||||
      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
					      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
				
			||||||
@@ -186,17 +186,17 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def conflictForNew: Constraint = new Constraint(){
 | 
					  private def conflictForNew: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] = {
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] = {
 | 
				
			||||||
      optionIf(targetWikiPage.nonEmpty){
 | 
					      targetWikiPage.map { _ =>
 | 
				
			||||||
        Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
 | 
					        "Someone has created the wiki since you started. Please reload this page and re-apply your changes."
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def conflictForEdit: Constraint = new Constraint(){
 | 
					  private def conflictForEdit: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] = {
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] = {
 | 
				
			||||||
      optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
 | 
					      targetWikiPage.filter(_.id != params("id")).map{ _ =>
 | 
				
			||||||
        Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
 | 
					        "Someone has edited the wiki since you started. Please reload this page and re-apply your changes."
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,26 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait AccountComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Accounts extends Table[Account]("ACCOUNT") {
 | 
					  lazy val Accounts = TableQuery[Accounts]
 | 
				
			||||||
  def userName = column[String]("USER_NAME", O PrimaryKey)
 | 
					
 | 
				
			||||||
  def fullName = column[String]("FULL_NAME")
 | 
					  class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") {
 | 
				
			||||||
  def mailAddress = column[String]("MAIL_ADDRESS")
 | 
					    val userName = column[String]("USER_NAME", O PrimaryKey)
 | 
				
			||||||
  def password = column[String]("PASSWORD")
 | 
					    val fullName = column[String]("FULL_NAME")
 | 
				
			||||||
  def isAdmin = column[Boolean]("ADMINISTRATOR")
 | 
					    val mailAddress = column[String]("MAIL_ADDRESS")
 | 
				
			||||||
  def url = column[String]("URL")
 | 
					    val password = column[String]("PASSWORD")
 | 
				
			||||||
  def registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
					    val isAdmin = column[Boolean]("ADMINISTRATOR")
 | 
				
			||||||
  def updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
					    val url = column[String]("URL")
 | 
				
			||||||
  def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
 | 
					    val registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
				
			||||||
  def image = column[String]("IMAGE")
 | 
					    val updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
				
			||||||
  def groupAccount = column[Boolean]("GROUP_ACCOUNT")
 | 
					    val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
 | 
				
			||||||
  def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
 | 
					    val image = column[String]("IMAGE")
 | 
				
			||||||
 | 
					    val groupAccount = column[Boolean]("GROUP_ACCOUNT")
 | 
				
			||||||
 | 
					    val removed = column[Boolean]("REMOVED")
 | 
				
			||||||
 | 
					    def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Account(
 | 
					case class Account(
 | 
				
			||||||
@@ -28,5 +34,6 @@ case class Account(
 | 
				
			|||||||
  updatedDate: java.util.Date,
 | 
					  updatedDate: java.util.Date,
 | 
				
			||||||
  lastLoginDate: Option[java.util.Date],
 | 
					  lastLoginDate: Option[java.util.Date],
 | 
				
			||||||
  image: Option[String],
 | 
					  image: Option[String],
 | 
				
			||||||
    isGroupAccount: Boolean
 | 
					  isGroupAccount: Boolean,
 | 
				
			||||||
 | 
					  isRemoved: Boolean
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,31 +1,29 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait ActivityComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Activities extends Table[Activity]("ACTIVITY") with BasicTemplate {
 | 
					  lazy val Activities = TableQuery[Activities]
 | 
				
			||||||
  def activityId = column[Int]("ACTIVITY_ID", O AutoInc)
 | 
					 | 
				
			||||||
  def activityUserName = column[String]("ACTIVITY_USER_NAME")
 | 
					 | 
				
			||||||
  def activityType = column[String]("ACTIVITY_TYPE")
 | 
					 | 
				
			||||||
  def message = column[String]("MESSAGE")
 | 
					 | 
				
			||||||
  def additionalInfo = column[String]("ADDITIONAL_INFO")
 | 
					 | 
				
			||||||
  def activityDate = column[java.util.Date]("ACTIVITY_DATE")
 | 
					 | 
				
			||||||
  def * = activityId ~ userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate <> (Activity, Activity.unapply _)
 | 
					 | 
				
			||||||
  def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate {
 | 
					  class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
 | 
				
			||||||
  def commitId = column[String]("COMMIT_ID")
 | 
					    val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
 | 
				
			||||||
  def * = userName ~ repositoryName ~ commitId
 | 
					    val activityUserName = column[String]("ACTIVITY_USER_NAME")
 | 
				
			||||||
  def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind)
 | 
					    val activityType = column[String]("ACTIVITY_TYPE")
 | 
				
			||||||
 | 
					    val message = column[String]("MESSAGE")
 | 
				
			||||||
 | 
					    val additionalInfo = column[String]("ADDITIONAL_INFO")
 | 
				
			||||||
 | 
					    val activityDate = column[java.util.Date]("ACTIVITY_DATE")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Activity(
 | 
					case class Activity(
 | 
				
			||||||
  activityId: Int,
 | 
					 | 
				
			||||||
  userName: String,
 | 
					  userName: String,
 | 
				
			||||||
  repositoryName: String,
 | 
					  repositoryName: String,
 | 
				
			||||||
  activityUserName: String,
 | 
					  activityUserName: String,
 | 
				
			||||||
  activityType: String,
 | 
					  activityType: String,
 | 
				
			||||||
  message: String,
 | 
					  message: String,
 | 
				
			||||||
  additionalInfo: Option[String],
 | 
					  additionalInfo: Option[String],
 | 
				
			||||||
  activityDate: java.util.Date
 | 
					  activityDate: java.util.Date,
 | 
				
			||||||
 | 
					  activityId: Int = 0
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,44 +1,47 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					protected[model] trait TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
protected[model] trait BasicTemplate { self: Table[_] =>
 | 
					  trait BasicTemplate { self: Table[_] =>
 | 
				
			||||||
  def userName = column[String]("USER_NAME")
 | 
					    val userName = column[String]("USER_NAME")
 | 
				
			||||||
  def repositoryName = column[String]("REPOSITORY_NAME")
 | 
					    val repositoryName = column[String]("REPOSITORY_NAME")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byRepository(owner: String, repository: String) =
 | 
					    def byRepository(owner: String, repository: String) =
 | 
				
			||||||
    (userName is owner.bind) && (repositoryName is repository.bind)
 | 
					      (userName === owner.bind) && (repositoryName === repository.bind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byRepository(userName: Column[String], repositoryName: Column[String]) =
 | 
					    def byRepository(userName: Column[String], repositoryName: Column[String]) =
 | 
				
			||||||
    (this.userName is userName) && (this.repositoryName is repositoryName)
 | 
					      (this.userName === userName) && (this.repositoryName === repositoryName)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
protected[model] trait IssueTemplate extends BasicTemplate { self: Table[_] =>
 | 
					  trait IssueTemplate extends BasicTemplate { self: Table[_] =>
 | 
				
			||||||
  def issueId = column[Int]("ISSUE_ID")
 | 
					    val issueId = column[Int]("ISSUE_ID")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byIssue(owner: String, repository: String, issueId: Int) =
 | 
					    def byIssue(owner: String, repository: String, issueId: Int) =
 | 
				
			||||||
    byRepository(owner, repository) && (this.issueId is issueId.bind)
 | 
					      byRepository(owner, repository) && (this.issueId === issueId.bind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
 | 
					    def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) =
 | 
				
			||||||
    byRepository(userName, repositoryName) && (this.issueId is issueId)
 | 
					      byRepository(userName, repositoryName) && (this.issueId === issueId)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
protected[model] trait LabelTemplate extends BasicTemplate { self: Table[_] =>
 | 
					  trait LabelTemplate extends BasicTemplate { self: Table[_] =>
 | 
				
			||||||
  def labelId = column[Int]("LABEL_ID")
 | 
					    val labelId = column[Int]("LABEL_ID")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byLabel(owner: String, repository: String, labelId: Int) =
 | 
					    def byLabel(owner: String, repository: String, labelId: Int) =
 | 
				
			||||||
    byRepository(owner, repository) && (this.labelId is labelId.bind)
 | 
					      byRepository(owner, repository) && (this.labelId === labelId.bind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
 | 
					    def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
 | 
				
			||||||
    byRepository(userName, repositoryName) && (this.labelId is labelId)
 | 
					      byRepository(userName, repositoryName) && (this.labelId === labelId)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
protected[model] trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
 | 
					  trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
 | 
				
			||||||
  def milestoneId = column[Int]("MILESTONE_ID")
 | 
					    val milestoneId = column[Int]("MILESTONE_ID")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byMilestone(owner: String, repository: String, milestoneId: Int) =
 | 
					    def byMilestone(owner: String, repository: String, milestoneId: Int) =
 | 
				
			||||||
    byRepository(owner, repository) && (this.milestoneId is milestoneId.bind)
 | 
					      byRepository(owner, repository) && (this.milestoneId === milestoneId.bind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
 | 
					    def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) =
 | 
				
			||||||
    byRepository(userName, repositoryName) && (this.milestoneId is milestoneId)
 | 
					      byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,17 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait CollaboratorComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Collaborators extends Table[Collaborator]("COLLABORATOR") with BasicTemplate {
 | 
					  lazy val Collaborators = TableQuery[Collaborators]
 | 
				
			||||||
  def collaboratorName = column[String]("COLLABORATOR_NAME")
 | 
					
 | 
				
			||||||
  def * = userName ~ repositoryName ~ collaboratorName <> (Collaborator, Collaborator.unapply _)
 | 
					  class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
 | 
				
			||||||
 | 
					    val collaboratorName = column[String]("COLLABORATOR_NAME")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String, collaborator: String) =
 | 
					    def byPrimaryKey(owner: String, repository: String, collaborator: String) =
 | 
				
			||||||
    byRepository(owner, repository) && (collaboratorName is collaborator.bind)
 | 
					      byRepository(owner, repository) && (collaboratorName === collaborator.bind)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Collaborator(
 | 
					case class Collaborator(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,20 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait GroupMemberComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
 | 
					  lazy val GroupMembers = TableQuery[GroupMembers]
 | 
				
			||||||
  def groupName = column[String]("GROUP_NAME", O PrimaryKey)
 | 
					
 | 
				
			||||||
  def userName = column[String]("USER_NAME", O PrimaryKey)
 | 
					  class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") {
 | 
				
			||||||
  def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
 | 
					    val groupName = column[String]("GROUP_NAME", O PrimaryKey)
 | 
				
			||||||
 | 
					    val userName = column[String]("USER_NAME", O PrimaryKey)
 | 
				
			||||||
 | 
					    val isManager = column[Boolean]("MANAGER")
 | 
				
			||||||
 | 
					    def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class GroupMember(
 | 
					case class GroupMember(
 | 
				
			||||||
  groupName: String,
 | 
					  groupName: String,
 | 
				
			||||||
  userName: String
 | 
					  userName: String,
 | 
				
			||||||
 | 
					  isManager: Boolean
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -1,29 +1,36 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait IssueComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object IssueId extends Table[(String, String, Int)]("ISSUE_ID") with IssueTemplate {
 | 
					  lazy val IssueId = TableQuery[IssueId]
 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId
 | 
					  lazy val IssueOutline = TableQuery[IssueOutline]
 | 
				
			||||||
 | 
					  lazy val Issues = TableQuery[Issues]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate {
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, issueId)
 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
 | 
					    def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
 | 
					  class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate {
 | 
				
			||||||
  def commentCount = column[Int]("COMMENT_COUNT")
 | 
					    val commentCount = column[Int]("COMMENT_COUNT")
 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId ~ commentCount
 | 
					    def * = (userName, repositoryName, issueId, commentCount)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
 | 
					  class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate {
 | 
				
			||||||
  def openedUserName = column[String]("OPENED_USER_NAME")
 | 
					    val openedUserName = column[String]("OPENED_USER_NAME")
 | 
				
			||||||
  def assignedUserName = column[String]("ASSIGNED_USER_NAME")
 | 
					    val assignedUserName = column[String]("ASSIGNED_USER_NAME")
 | 
				
			||||||
  def title = column[String]("TITLE")
 | 
					    val title = column[String]("TITLE")
 | 
				
			||||||
  def content = column[String]("CONTENT")
 | 
					    val content = column[String]("CONTENT")
 | 
				
			||||||
  def closed = column[Boolean]("CLOSED")
 | 
					    val closed = column[Boolean]("CLOSED")
 | 
				
			||||||
  def registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
					    val registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
				
			||||||
  def updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
					    val updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
				
			||||||
  def pullRequest = column[Boolean]("PULL_REQUEST")
 | 
					    val pullRequest = column[Boolean]("PULL_REQUEST")
 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
 | 
					    def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
 | 
					    def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Issue(
 | 
					case class Issue(
 | 
				
			||||||
@@ -38,4 +45,5 @@ case class Issue(
 | 
				
			|||||||
  closed: Boolean,
 | 
					  closed: Boolean,
 | 
				
			||||||
  registeredDate: java.util.Date,
 | 
					  registeredDate: java.util.Date,
 | 
				
			||||||
  updatedDate: java.util.Date,
 | 
					  updatedDate: java.util.Date,
 | 
				
			||||||
    isPullRequest: Boolean)
 | 
					  isPullRequest: Boolean
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,25 +1,31 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait IssueCommentComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object IssueComments extends Table[IssueComment]("ISSUE_COMMENT") with IssueTemplate {
 | 
					  lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
 | 
				
			||||||
  def commentId = column[Int]("COMMENT_ID", O AutoInc)
 | 
					    def autoInc = this returning this.map(_.commentId)
 | 
				
			||||||
  def action = column[String]("ACTION")
 | 
					  }
 | 
				
			||||||
  def commentedUserName = column[String]("COMMENTED_USER_NAME")
 | 
					 | 
				
			||||||
  def content = column[String]("CONTENT")
 | 
					 | 
				
			||||||
  def registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
					 | 
				
			||||||
  def updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
					 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId ~ commentId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate <> (IssueComment, IssueComment.unapply _)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def autoInc = userName ~ repositoryName ~ issueId ~ action ~ commentedUserName ~ content ~ registeredDate ~ updatedDate returning commentId
 | 
					  class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
 | 
				
			||||||
  def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind
 | 
					    val commentId = column[Int]("COMMENT_ID", O AutoInc)
 | 
				
			||||||
 | 
					    val action = column[String]("ACTION")
 | 
				
			||||||
 | 
					    val commentedUserName = column[String]("COMMENTED_USER_NAME")
 | 
				
			||||||
 | 
					    val content = column[String]("CONTENT")
 | 
				
			||||||
 | 
					    val registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
				
			||||||
 | 
					    val updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class IssueComment(
 | 
					case class IssueComment(
 | 
				
			||||||
  userName: String,
 | 
					  userName: String,
 | 
				
			||||||
  repositoryName: String,
 | 
					  repositoryName: String,
 | 
				
			||||||
  issueId: Int,
 | 
					  issueId: Int,
 | 
				
			||||||
    commentId: Int,
 | 
					  commentId: Int = 0,
 | 
				
			||||||
  action: String,
 | 
					  action: String,
 | 
				
			||||||
  commentedUserName: String,
 | 
					  commentedUserName: String,
 | 
				
			||||||
  content: String,
 | 
					  content: String,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,20 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait IssueLabelComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object IssueLabels extends Table[IssueLabel]("ISSUE_LABEL") with IssueTemplate with LabelTemplate {
 | 
					  lazy val IssueLabels = TableQuery[IssueLabels]
 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId ~ labelId <> (IssueLabel, IssueLabel.unapply _)
 | 
					
 | 
				
			||||||
 | 
					  class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate {
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply)
 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
 | 
					    def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) =
 | 
				
			||||||
    byIssue(owner, repository, issueId) && (this.labelId is labelId.bind)
 | 
					      byIssue(owner, repository, issueId) && (this.labelId === labelId.bind)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class IssueLabel(
 | 
					case class IssueLabel(
 | 
				
			||||||
  userName: String,
 | 
					  userName: String,
 | 
				
			||||||
  repositoryName: String,
 | 
					  repositoryName: String,
 | 
				
			||||||
  issueId: Int,
 | 
					  issueId: Int,
 | 
				
			||||||
  labelId: Int)
 | 
					  labelId: Int
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,25 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait LabelComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Labels extends Table[Label]("LABEL") with LabelTemplate {
 | 
					  lazy val Labels = TableQuery[Labels]
 | 
				
			||||||
  def labelName = column[String]("LABEL_NAME")
 | 
					
 | 
				
			||||||
  def color = column[String]("COLOR")
 | 
					  class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
 | 
				
			||||||
  def * = userName ~ repositoryName ~ labelId ~ labelName ~ color <> (Label, Label.unapply _)
 | 
					    override val labelId = column[Int]("LABEL_ID", O AutoInc)
 | 
				
			||||||
 | 
					    val labelName = column[String]("LABEL_NAME")
 | 
				
			||||||
 | 
					    val color = column[String]("COLOR")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def ins = userName ~ repositoryName ~ labelName ~ color
 | 
					 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
 | 
					    def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId)
 | 
				
			||||||
    def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
 | 
					    def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Label(
 | 
					case class Label(
 | 
				
			||||||
  userName: String,
 | 
					  userName: String,
 | 
				
			||||||
  repositoryName: String,
 | 
					  repositoryName: String,
 | 
				
			||||||
  labelId: Int,
 | 
					  labelId: Int = 0,
 | 
				
			||||||
  labelName: String,
 | 
					  labelName: String,
 | 
				
			||||||
  color: String){
 | 
					  color: String){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,5 +34,4 @@ case class Label(
 | 
				
			|||||||
      "FFFFFF"
 | 
					      "FFFFFF"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,24 +1,30 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait MilestoneComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Milestones extends Table[Milestone]("MILESTONE") with MilestoneTemplate {
 | 
					  lazy val Milestones = TableQuery[Milestones]
 | 
				
			||||||
  def title = column[String]("TITLE")
 | 
					
 | 
				
			||||||
  def description = column[String]("DESCRIPTION")
 | 
					  class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate {
 | 
				
			||||||
  def dueDate = column[java.util.Date]("DUE_DATE")
 | 
					    override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc)
 | 
				
			||||||
  def closedDate = column[java.util.Date]("CLOSED_DATE")
 | 
					    val title = column[String]("TITLE")
 | 
				
			||||||
  def * = userName ~ repositoryName ~ milestoneId ~ title ~ description.? ~ dueDate.? ~ closedDate.? <> (Milestone, Milestone.unapply _)
 | 
					    val description = column[String]("DESCRIPTION")
 | 
				
			||||||
 | 
					    val dueDate = column[java.util.Date]("DUE_DATE")
 | 
				
			||||||
 | 
					    val closedDate = column[java.util.Date]("CLOSED_DATE")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def ins = userName ~ repositoryName ~ title ~ description.? ~ dueDate.? ~ closedDate.?
 | 
					 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
 | 
					    def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId)
 | 
				
			||||||
    def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
 | 
					    def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Milestone(
 | 
					case class Milestone(
 | 
				
			||||||
  userName: String,
 | 
					  userName: String,
 | 
				
			||||||
  repositoryName: String,
 | 
					  repositoryName: String,
 | 
				
			||||||
  milestoneId: Int,
 | 
					  milestoneId: Int = 0,
 | 
				
			||||||
  title: String,
 | 
					  title: String,
 | 
				
			||||||
  description: Option[String],
 | 
					  description: Option[String],
 | 
				
			||||||
  dueDate: Option[java.util.Date],
 | 
					  dueDate: Option[java.util.Date],
 | 
				
			||||||
  closedDate: Option[java.util.Date])
 | 
					  closedDate: Option[java.util.Date]
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/main/scala/model/Plugin.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/main/scala/model/Plugin.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait PluginComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  lazy val Plugins = TableQuery[Plugins]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){
 | 
				
			||||||
 | 
					    val pluginId = column[String]("PLUGIN_ID", O PrimaryKey)
 | 
				
			||||||
 | 
					    val version = column[String]("VERSION")
 | 
				
			||||||
 | 
					    def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					case class Plugin(
 | 
				
			||||||
 | 
					  pluginId: String,
 | 
				
			||||||
 | 
					  version: String
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										42
									
								
								src/main/scala/model/Profile.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/main/scala/model/Profile.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait Profile {
 | 
				
			||||||
 | 
					  val profile: slick.driver.JdbcProfile
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // java.util.Date Mapped Column Types
 | 
				
			||||||
 | 
					  implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp](
 | 
				
			||||||
 | 
					      d => new java.sql.Timestamp(d.getTime),
 | 
				
			||||||
 | 
					      t => new java.util.Date(t.getTime)
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  implicit class RichColumn(c1: Column[Boolean]){
 | 
				
			||||||
 | 
					    def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object Profile extends {
 | 
				
			||||||
 | 
					  val profile = slick.driver.H2Driver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} with AccountComponent
 | 
				
			||||||
 | 
					  with ActivityComponent
 | 
				
			||||||
 | 
					  with CollaboratorComponent
 | 
				
			||||||
 | 
					  with GroupMemberComponent
 | 
				
			||||||
 | 
					  with IssueComponent
 | 
				
			||||||
 | 
					  with IssueCommentComponent
 | 
				
			||||||
 | 
					  with IssueLabelComponent
 | 
				
			||||||
 | 
					  with LabelComponent
 | 
				
			||||||
 | 
					  with MilestoneComponent
 | 
				
			||||||
 | 
					  with PullRequestComponent
 | 
				
			||||||
 | 
					  with RepositoryComponent
 | 
				
			||||||
 | 
					  with SshKeyComponent
 | 
				
			||||||
 | 
					  with WebHookComponent
 | 
				
			||||||
 | 
					  with PluginComponent with Profile {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns system date.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def currentDate = new java.util.Date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,18 +1,22 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait PullRequestComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
 | 
					  lazy val PullRequests = TableQuery[PullRequests]
 | 
				
			||||||
  def branch = column[String]("BRANCH")
 | 
					
 | 
				
			||||||
  def requestUserName = column[String]("REQUEST_USER_NAME")
 | 
					  class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate {
 | 
				
			||||||
  def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
 | 
					    val branch = column[String]("BRANCH")
 | 
				
			||||||
  def requestBranch = column[String]("REQUEST_BRANCH")
 | 
					    val requestUserName = column[String]("REQUEST_USER_NAME")
 | 
				
			||||||
  def commitIdFrom = column[String]("COMMIT_ID_FROM")
 | 
					    val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
 | 
				
			||||||
  def commitIdTo = column[String]("COMMIT_ID_TO")
 | 
					    val requestBranch = column[String]("REQUEST_BRANCH")
 | 
				
			||||||
  def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
 | 
					    val commitIdFrom = column[String]("COMMIT_ID_FROM")
 | 
				
			||||||
 | 
					    val commitIdTo = column[String]("COMMIT_ID_TO")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
 | 
					    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)
 | 
					    def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class PullRequest(
 | 
					case class PullRequest(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,26 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait RepositoryComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					  import self._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Repositories extends Table[Repository]("REPOSITORY") with BasicTemplate {
 | 
					  lazy val Repositories = TableQuery[Repositories]
 | 
				
			||||||
  def isPrivate = column[Boolean]("PRIVATE")
 | 
					
 | 
				
			||||||
  def description = column[String]("DESCRIPTION")
 | 
					  class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate {
 | 
				
			||||||
  def defaultBranch = column[String]("DEFAULT_BRANCH")
 | 
					    val isPrivate = column[Boolean]("PRIVATE")
 | 
				
			||||||
  def registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
					    val description = column[String]("DESCRIPTION")
 | 
				
			||||||
  def updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
					    val defaultBranch = column[String]("DEFAULT_BRANCH")
 | 
				
			||||||
  def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
 | 
					    val registeredDate = column[java.util.Date]("REGISTERED_DATE")
 | 
				
			||||||
  def originUserName = column[String]("ORIGIN_USER_NAME")
 | 
					    val updatedDate = column[java.util.Date]("UPDATED_DATE")
 | 
				
			||||||
  def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
 | 
					    val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
 | 
				
			||||||
  def parentUserName = column[String]("PARENT_USER_NAME")
 | 
					    val originUserName = column[String]("ORIGIN_USER_NAME")
 | 
				
			||||||
  def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
 | 
					    val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
 | 
				
			||||||
  def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
 | 
					    val parentUserName = column[String]("PARENT_USER_NAME")
 | 
				
			||||||
 | 
					    val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
 | 
					    def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class Repository(
 | 
					case class Repository(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								src/main/scala/model/SshKey.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/main/scala/model/SshKey.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait SshKeyComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  lazy val SshKeys = TableQuery[SshKeys]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") {
 | 
				
			||||||
 | 
					    val userName = column[String]("USER_NAME")
 | 
				
			||||||
 | 
					    val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc)
 | 
				
			||||||
 | 
					    val title = column[String]("TITLE")
 | 
				
			||||||
 | 
					    val publicKey = column[String]("PUBLIC_KEY")
 | 
				
			||||||
 | 
					    def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					case class SshKey(
 | 
				
			||||||
 | 
					  userName: String,
 | 
				
			||||||
 | 
					  sshKeyId: Int = 0,
 | 
				
			||||||
 | 
					  title: String,
 | 
				
			||||||
 | 
					  publicKey: String
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -1,12 +1,16 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					trait WebHookComponent extends TemplateComponent { self: Profile =>
 | 
				
			||||||
 | 
					  import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
 | 
					  lazy val WebHooks = TableQuery[WebHooks]
 | 
				
			||||||
  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)
 | 
					  class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
 | 
				
			||||||
 | 
					    val url = column[String]("URL")
 | 
				
			||||||
 | 
					    def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
case class WebHook(
 | 
					case class WebHook(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,3 @@
 | 
				
			|||||||
package object model {
 | 
					package object model {
 | 
				
			||||||
  import scala.slick.driver.BasicDriver.Implicit._
 | 
					  type Session = slick.jdbc.JdbcBackend#Session
 | 
				
			||||||
  import scala.slick.lifted.{Column, MappedTypeMapper}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // java.util.Date TypeMapper
 | 
					 | 
				
			||||||
  implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
 | 
					 | 
				
			||||||
      d => new java.sql.Timestamp(d.getTime),
 | 
					 | 
				
			||||||
      t => new java.util.Date(t.getTime)
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  implicit class RichColumn(c1: Column[Boolean]){
 | 
					 | 
				
			||||||
    def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Returns system date.
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  def currentDate = new java.util.Date()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/main/scala/plugin/Plugin.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/main/scala/plugin/Plugin.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					package plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import plugin.PluginSystem._
 | 
				
			||||||
 | 
					import java.sql.Connection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait Plugin {
 | 
				
			||||||
 | 
					  val id: String
 | 
				
			||||||
 | 
					  val version: String
 | 
				
			||||||
 | 
					  val author: String
 | 
				
			||||||
 | 
					  val url: String
 | 
				
			||||||
 | 
					  val description: String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def repositoryMenus       : List[RepositoryMenu]
 | 
				
			||||||
 | 
					  def globalMenus           : List[GlobalMenu]
 | 
				
			||||||
 | 
					  def repositoryActions     : List[RepositoryAction]
 | 
				
			||||||
 | 
					  def globalActions         : List[Action]
 | 
				
			||||||
 | 
					  def javaScripts           : List[JavaScript]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PluginConnectionHolder {
 | 
				
			||||||
 | 
					  val threadLocal = new ThreadLocal[Connection]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										194
									
								
								src/main/scala/plugin/PluginSystem.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/main/scala/plugin/PluginSystem.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
				
			|||||||
 | 
					package plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicBoolean
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import org.apache.commons.io.{IOUtils, FileUtils}
 | 
				
			||||||
 | 
					import Security._
 | 
				
			||||||
 | 
					import service.PluginService
 | 
				
			||||||
 | 
					import model.Profile._
 | 
				
			||||||
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import java.io.FileInputStream
 | 
				
			||||||
 | 
					import java.sql.Connection
 | 
				
			||||||
 | 
					import app.Context
 | 
				
			||||||
 | 
					import service.RepositoryService.RepositoryInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Provides extension points to plug-ins.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					object PluginSystem extends PluginService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val initialized = new AtomicBoolean(false)
 | 
				
			||||||
 | 
					  private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
 | 
				
			||||||
 | 
					  private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def install(plugin: Plugin): Unit = {
 | 
				
			||||||
 | 
					    pluginsMap.put(plugin.id, plugin)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def plugins: List[Plugin] = pluginsMap.values.toList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def uninstall(id: String)(implicit session: Session): Unit = {
 | 
				
			||||||
 | 
					    pluginsMap.remove(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Delete from PLUGIN table
 | 
				
			||||||
 | 
					    deletePlugin(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Drop tables
 | 
				
			||||||
 | 
					    val pluginDir = new java.io.File(PluginHome)
 | 
				
			||||||
 | 
					    val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
 | 
				
			||||||
 | 
					    if(sqlFile.exists){
 | 
				
			||||||
 | 
					      val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
 | 
				
			||||||
 | 
					      using(session.conn.createStatement()){ stmt =>
 | 
				
			||||||
 | 
					        stmt.executeUpdate(sql)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def repositories: List[PluginRepository] = repositoriesList.toList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def init()(implicit session: Session): Unit = {
 | 
				
			||||||
 | 
					    if(initialized.compareAndSet(false, true)){
 | 
				
			||||||
 | 
					      // Load installed plugins
 | 
				
			||||||
 | 
					      val pluginDir = new java.io.File(PluginHome)
 | 
				
			||||||
 | 
					      if(pluginDir.exists && pluginDir.isDirectory){
 | 
				
			||||||
 | 
					        pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
 | 
				
			||||||
 | 
					          installPlugin(dir.getName)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Add default plugin repositories
 | 
				
			||||||
 | 
					      repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO Method name seems to not so good.
 | 
				
			||||||
 | 
					  def installPlugin(id: String)(implicit session: Session): Unit = {
 | 
				
			||||||
 | 
					    val pluginHome = new java.io.File(PluginHome)
 | 
				
			||||||
 | 
					    val pluginDir  = new java.io.File(pluginHome, id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val scalaFile = new java.io.File(pluginDir, "plugin.scala")
 | 
				
			||||||
 | 
					    if(scalaFile.exists && scalaFile.isFile){
 | 
				
			||||||
 | 
					      val properties = new java.util.Properties()
 | 
				
			||||||
 | 
					      using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
 | 
				
			||||||
 | 
					        properties.load(in)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val pluginId     = properties.getProperty("id")
 | 
				
			||||||
 | 
					      val version      = properties.getProperty("version")
 | 
				
			||||||
 | 
					      val author       = properties.getProperty("author")
 | 
				
			||||||
 | 
					      val url          = properties.getProperty("url")
 | 
				
			||||||
 | 
					      val description  = properties.getProperty("description")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val source = s"""
 | 
				
			||||||
 | 
					        |val id          = "${pluginId}"
 | 
				
			||||||
 | 
					        |val version     = "${version}"
 | 
				
			||||||
 | 
					        |val author      = "${author}"
 | 
				
			||||||
 | 
					        |val url         = "${url}"
 | 
				
			||||||
 | 
					        |val description = "${description}"
 | 
				
			||||||
 | 
					      """.stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // Compile and eval Scala source code
 | 
				
			||||||
 | 
					        ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
 | 
				
			||||||
 | 
					          ScalaPlugin.compileTemplate(
 | 
				
			||||||
 | 
					            id.replaceAll("-", ""),
 | 
				
			||||||
 | 
					            file.getName.replaceAll("\\.scala\\.html$", ""),
 | 
				
			||||||
 | 
					            IOUtils.toString(new FileInputStream(file)))
 | 
				
			||||||
 | 
					        }.mkString("\n") + source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Migrate database
 | 
				
			||||||
 | 
					        val plugin = getPlugin(pluginId)
 | 
				
			||||||
 | 
					        if(plugin.isEmpty){
 | 
				
			||||||
 | 
					          registerPlugin(model.Plugin(pluginId, version))
 | 
				
			||||||
 | 
					          migrate(session.conn, pluginId, "0.0")
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          updatePlugin(model.Plugin(pluginId, version))
 | 
				
			||||||
 | 
					          migrate(session.conn, pluginId, plugin.get.version)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO Should PluginSystem provide a way to migrate resources other than H2?
 | 
				
			||||||
 | 
					  private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
 | 
				
			||||||
 | 
					    val pluginDir = new java.io.File(PluginHome)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO Is ot possible to use this migration system in GitBucket migration?
 | 
				
			||||||
 | 
					    val dim = current.split("\\.")
 | 
				
			||||||
 | 
					    val currentVersion = Version(dim(0).toInt, dim(1).toInt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
 | 
				
			||||||
 | 
					    if(sqlDir.exists && sqlDir.isDirectory){
 | 
				
			||||||
 | 
					      sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
 | 
				
			||||||
 | 
					        val array = file.getName.replaceFirst("\\.sql", "").split("_")
 | 
				
			||||||
 | 
					        Version(array(0).toInt, array(1).toInt)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .sorted.reverse.takeWhile(_ > currentVersion)
 | 
				
			||||||
 | 
					      .reverse.foreach { version =>
 | 
				
			||||||
 | 
					        val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
 | 
				
			||||||
 | 
					        val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
 | 
				
			||||||
 | 
					        using(conn.createStatement()){ stmt =>
 | 
				
			||||||
 | 
					          stmt.executeUpdate(sql)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class Version(major: Int, minor: Int) extends Ordered[Version] {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def compare(that: Version): Int = {
 | 
				
			||||||
 | 
					      if(major != that.major){
 | 
				
			||||||
 | 
					        major.compare(that.major)
 | 
				
			||||||
 | 
					      } else{
 | 
				
			||||||
 | 
					        minor.compare(that.minor)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def displayString: String = major + "." + minor
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def repositoryMenus       : List[RepositoryMenu]   = pluginsMap.values.flatMap(_.repositoryMenus).toList
 | 
				
			||||||
 | 
					  def globalMenus           : List[GlobalMenu]       = pluginsMap.values.flatMap(_.globalMenus).toList
 | 
				
			||||||
 | 
					  def repositoryActions     : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
 | 
				
			||||||
 | 
					  def globalActions         : List[Action]           = pluginsMap.values.flatMap(_.globalActions).toList
 | 
				
			||||||
 | 
					  def javaScripts           : List[JavaScript]       = pluginsMap.values.flatMap(_.javaScripts).toList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Case classes to hold plug-ins information internally in GitBucket
 | 
				
			||||||
 | 
					  case class PluginRepository(id: String, url: String)
 | 
				
			||||||
 | 
					  case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
 | 
				
			||||||
 | 
					  case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
 | 
				
			||||||
 | 
					  case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
 | 
				
			||||||
 | 
					  case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
 | 
				
			||||||
 | 
					  case class Button(label: String, href: String)
 | 
				
			||||||
 | 
					  case class JavaScript(filter: String => Boolean, script: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Checks whether the plugin is updatable.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
 | 
				
			||||||
 | 
					    if(oldVersion == newVersion){
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      val dim1 = oldVersion.split("\\.").map(_.toInt)
 | 
				
			||||||
 | 
					      val dim2 = newVersion.split("\\.").map(_.toInt)
 | 
				
			||||||
 | 
					      dim1.zip(dim2).foreach { case (a, b) =>
 | 
				
			||||||
 | 
					        if(a < b){
 | 
				
			||||||
 | 
					          return true
 | 
				
			||||||
 | 
					        } else if(a > b){
 | 
				
			||||||
 | 
					          return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								src/main/scala/plugin/PluginUpdateJob.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/main/scala/plugin/PluginUpdateJob.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					package plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import org.quartz.{Scheduler, JobExecutionContext, Job}
 | 
				
			||||||
 | 
					import org.quartz.JobBuilder._
 | 
				
			||||||
 | 
					import org.quartz.TriggerBuilder._
 | 
				
			||||||
 | 
					import org.quartz.SimpleScheduleBuilder._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PluginUpdateJob extends Job {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
 | 
				
			||||||
 | 
					  private var failedCount = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Clone or pull all plugin repositories
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * TODO Support plugin repository access through the proxy server
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  override def execute(context: JobExecutionContext): Unit = {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if(failedCount > 3){
 | 
				
			||||||
 | 
					        logger.error("Skip plugin information updating because failed count is over limit")
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        logger.info("Start plugin information updating")
 | 
				
			||||||
 | 
					        PluginSystem.repositories.foreach { repository =>
 | 
				
			||||||
 | 
					          logger.info(s"Updating ${repository.id}: ${repository.url}...")
 | 
				
			||||||
 | 
					          val dir = getPluginCacheDir()
 | 
				
			||||||
 | 
					          val repo = new java.io.File(dir, repository.id)
 | 
				
			||||||
 | 
					          if(repo.exists){
 | 
				
			||||||
 | 
					            // pull if the repository is already cloned
 | 
				
			||||||
 | 
					            Git.open(repo).pull().call()
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // clone if the repository is not exist
 | 
				
			||||||
 | 
					            Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        logger.info("End plugin information updating")
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      case e: Exception => {
 | 
				
			||||||
 | 
					        failedCount = failedCount + 1
 | 
				
			||||||
 | 
					        logger.error("Failed to update plugin information", e)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PluginUpdateJob {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def schedule(scheduler: Scheduler): Unit = {
 | 
				
			||||||
 | 
					    val job = newJob(classOf[PluginUpdateJob])
 | 
				
			||||||
 | 
					      .withIdentity("pluginUpdateJob")
 | 
				
			||||||
 | 
					      .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val trigger = newTrigger()
 | 
				
			||||||
 | 
					      .withIdentity("pluginUpdateTrigger")
 | 
				
			||||||
 | 
					      .startNow()
 | 
				
			||||||
 | 
					      .withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
 | 
				
			||||||
 | 
					      .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    scheduler.scheduleJob(job, trigger)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								src/main/scala/plugin/ScalaPlugin.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/main/scala/plugin/ScalaPlugin.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					package plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import scala.collection.mutable.ListBuffer
 | 
				
			||||||
 | 
					import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 | 
				
			||||||
 | 
					import app.Context
 | 
				
			||||||
 | 
					import plugin.PluginSystem._
 | 
				
			||||||
 | 
					import plugin.PluginSystem.RepositoryMenu
 | 
				
			||||||
 | 
					import plugin.Security._
 | 
				
			||||||
 | 
					import service.RepositoryService.RepositoryInfo
 | 
				
			||||||
 | 
					import scala.reflect.runtime.currentMirror
 | 
				
			||||||
 | 
					import scala.tools.reflect.ToolBox
 | 
				
			||||||
 | 
					import play.twirl.compiler.TwirlCompiler
 | 
				
			||||||
 | 
					import scala.io.Codec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO This is a sample implementation for Scala based plug-ins.
 | 
				
			||||||
 | 
					class ScalaPlugin(val id: String, val version: String,
 | 
				
			||||||
 | 
					                  val author: String, val url: String, val description: String) extends Plugin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val repositoryMenuList   = ListBuffer[RepositoryMenu]()
 | 
				
			||||||
 | 
					  private val globalMenuList       = ListBuffer[GlobalMenu]()
 | 
				
			||||||
 | 
					  private val repositoryActionList = ListBuffer[RepositoryAction]()
 | 
				
			||||||
 | 
					  private val globalActionList     = ListBuffer[Action]()
 | 
				
			||||||
 | 
					  private val javaScriptList       = ListBuffer[JavaScript]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def repositoryMenus       : List[RepositoryMenu]   = repositoryMenuList.toList
 | 
				
			||||||
 | 
					  def globalMenus           : List[GlobalMenu]       = globalMenuList.toList
 | 
				
			||||||
 | 
					  def repositoryActions     : List[RepositoryAction] = repositoryActionList.toList
 | 
				
			||||||
 | 
					  def globalActions         : List[Action]           = globalActionList.toList
 | 
				
			||||||
 | 
					  def javaScripts           : List[JavaScript]       = javaScriptList.toList
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
 | 
				
			||||||
 | 
					    repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
 | 
				
			||||||
 | 
					    globalMenuList += GlobalMenu(label, url, icon, condition)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
 | 
				
			||||||
 | 
					    globalActionList += Action(method, path, security, function)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
 | 
				
			||||||
 | 
					    repositoryActionList += RepositoryAction(method, path, security, function)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addJavaScript(filter: String => Boolean, script: String): Unit = {
 | 
				
			||||||
 | 
					    javaScriptList += JavaScript(filter, script)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object ScalaPlugin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def define(id: String, version: String, author: String, url: String, description: String)
 | 
				
			||||||
 | 
					    = new ScalaPlugin(id, version, author, url, description)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def eval(source: String): Any = {
 | 
				
			||||||
 | 
					    val toolbox = currentMirror.mkToolBox()
 | 
				
			||||||
 | 
					    val tree = toolbox.parse(source)
 | 
				
			||||||
 | 
					    toolbox.eval(tree)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def compileTemplate(packageName: String, name: String, source: String): String = {
 | 
				
			||||||
 | 
					    val result = TwirlCompiler.parseAndGenerateCodeNewParser(
 | 
				
			||||||
 | 
					      Array(packageName, name),
 | 
				
			||||||
 | 
					      source.getBytes("UTF-8"),
 | 
				
			||||||
 | 
					      Codec(scala.util.Properties.sourceEncoding),
 | 
				
			||||||
 | 
					      "",
 | 
				
			||||||
 | 
					      "play.twirl.api.HtmlFormat.Appendable",
 | 
				
			||||||
 | 
					      "play.twirl.api.HtmlFormat",
 | 
				
			||||||
 | 
					      "",
 | 
				
			||||||
 | 
					      false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result.replaceFirst("package .*", "")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/main/scala/plugin/Security.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/main/scala/plugin/Security.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Defines enum case classes to specify permission for actions which is provided by plugin.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					object Security {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sealed trait Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * All users and guests
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class All() extends Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Only signed-in users
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class Login() extends Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Only repository owner and collaborators
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class Member() extends Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Only repository owner and managers of group repository
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class Owner() extends Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Only administrators
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class Admin() extends Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/main/scala/plugin/package.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/main/scala/plugin/package.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import java.sql.PreparedStatement
 | 
				
			||||||
 | 
					import play.twirl.api.Html
 | 
				
			||||||
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import scala.collection.mutable.ListBuffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package object plugin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class Redirect(path: String)
 | 
				
			||||||
 | 
					  case class Fragment(html: Html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object db {
 | 
				
			||||||
 | 
					    // TODO labelled place holder support
 | 
				
			||||||
 | 
					    def select(sql: String, params: Any*): Seq[Map[String, String]] = {
 | 
				
			||||||
 | 
					      defining(PluginConnectionHolder.threadLocal.get){ conn =>
 | 
				
			||||||
 | 
					        using(conn.prepareStatement(sql)){ stmt =>
 | 
				
			||||||
 | 
					          setParams(stmt, params: _*)
 | 
				
			||||||
 | 
					          using(stmt.executeQuery()){ rs =>
 | 
				
			||||||
 | 
					            val list = new ListBuffer[Map[String, String]]()
 | 
				
			||||||
 | 
					            while(rs.next){
 | 
				
			||||||
 | 
					              defining(rs.getMetaData){ meta =>
 | 
				
			||||||
 | 
					                val map = Range(1, meta.getColumnCount + 1).map { i =>
 | 
				
			||||||
 | 
					                  val name = meta.getColumnName(i)
 | 
				
			||||||
 | 
					                  (name, rs.getString(name))
 | 
				
			||||||
 | 
					                }.toMap
 | 
				
			||||||
 | 
					                list += map
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            list
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO labelled place holder support
 | 
				
			||||||
 | 
					    def update(sql: String, params: Any*): Int = {
 | 
				
			||||||
 | 
					      defining(PluginConnectionHolder.threadLocal.get){ conn =>
 | 
				
			||||||
 | 
					        using(conn.prepareStatement(sql)){ stmt =>
 | 
				
			||||||
 | 
					          setParams(stmt, params: _*)
 | 
				
			||||||
 | 
					          stmt.executeUpdate()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
 | 
				
			||||||
 | 
					      params.zipWithIndex.foreach { case (p, i) =>
 | 
				
			||||||
 | 
					        p match {
 | 
				
			||||||
 | 
					          case x: String  => stmt.setString(i + 1, x)
 | 
				
			||||||
 | 
					          case x: Int     => stmt.setInt(i + 1, x)
 | 
				
			||||||
 | 
					          case x: Boolean => stmt.setBoolean(i + 1, x)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,12 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import model._
 | 
					import model.Profile._
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import profile.simple._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import model.{Account, GroupMember}
 | 
				
			||||||
 | 
					// TODO [Slick 2.0]NOT import directly?
 | 
				
			||||||
 | 
					import model.Profile.dateColumnType
 | 
				
			||||||
import service.SystemSettingsService.SystemSettings
 | 
					import service.SystemSettingsService.SystemSettings
 | 
				
			||||||
import util.StringUtil._
 | 
					import util.StringUtil._
 | 
				
			||||||
import model.GroupMember
 | 
					 | 
				
			||||||
import scala.Some
 | 
					 | 
				
			||||||
import model.Account
 | 
					 | 
				
			||||||
import util.LDAPUtil
 | 
					import util.LDAPUtil
 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -15,7 +14,7 @@ trait AccountService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[AccountService])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[AccountService])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
 | 
					  def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] =
 | 
				
			||||||
    if(settings.ldapAuthentication){
 | 
					    if(settings.ldapAuthentication){
 | 
				
			||||||
      ldapAuthentication(settings, userName, password)
 | 
					      ldapAuthentication(settings, userName, password)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -25,7 +24,7 @@ trait AccountService {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Authenticate by internal database.
 | 
					   * Authenticate by internal database.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def defaultAuthentication(userName: String, password: String) = {
 | 
					  private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = {
 | 
				
			||||||
    getAccountByUserName(userName).collect {
 | 
					    getAccountByUserName(userName).collect {
 | 
				
			||||||
      case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
 | 
					      case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
 | 
				
			||||||
    } getOrElse None
 | 
					    } getOrElse None
 | 
				
			||||||
@@ -34,15 +33,39 @@ trait AccountService {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Authenticate by LDAP.
 | 
					   * Authenticate by LDAP.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
 | 
					  private def ldapAuthentication(settings: SystemSettings, userName: String, password: String)
 | 
				
			||||||
 | 
					                                (implicit s: Session): Option[Account] = {
 | 
				
			||||||
    LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
 | 
					    LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
 | 
				
			||||||
      case Right(mailAddress) => {
 | 
					      case Right(ldapUserInfo) => {
 | 
				
			||||||
        // Create or update account by LDAP information
 | 
					        // Create or update account by LDAP information
 | 
				
			||||||
        getAccountByUserName(userName) match {
 | 
					        getAccountByUserName(ldapUserInfo.userName, true) match {
 | 
				
			||||||
          case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
 | 
					          case Some(x) if(!x.isRemoved) => {
 | 
				
			||||||
          case None    => createAccount(userName, "", userName, mailAddress, false, None)
 | 
					            if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) {
 | 
				
			||||||
 | 
					              updateAccount(x.copy(fullName = ldapUserInfo.fullName))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            getAccountByUserName(ldapUserInfo.userName)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          case Some(x) if(x.isRemoved)  => {
 | 
				
			||||||
 | 
					            logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
 | 
				
			||||||
 | 
					            defaultAuthentication(userName, password)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match {
 | 
				
			||||||
 | 
					            case Some(x) if(!x.isRemoved) => {
 | 
				
			||||||
 | 
					              updateAccount(x.copy(fullName = ldapUserInfo.fullName))
 | 
				
			||||||
 | 
					              getAccountByUserName(ldapUserInfo.userName)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            case Some(x) if(x.isRemoved)  => {
 | 
				
			||||||
 | 
					              logger.info("LDAP Authentication Failed: Account is already registered but disabled.")
 | 
				
			||||||
 | 
					              defaultAuthentication(userName, password)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            case None => {
 | 
				
			||||||
 | 
					              createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None)
 | 
				
			||||||
 | 
					              getAccountByUserName(ldapUserInfo.userName)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        getAccountByUserName(userName)
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      case Left(errorMessage) => {
 | 
					      case Left(errorMessage) => {
 | 
				
			||||||
        logger.info(s"LDAP Authentication Failed: ${errorMessage}")
 | 
					        logger.info(s"LDAP Authentication Failed: ${errorMessage}")
 | 
				
			||||||
@@ -51,15 +74,21 @@ trait AccountService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getAccountByUserName(userName: String): Option[Account] = 
 | 
					  def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
 | 
				
			||||||
    Query(Accounts) filter(_.userName is userName.bind) firstOption
 | 
					    Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getAccountByMailAddress(mailAddress: String): Option[Account] =
 | 
					  def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
 | 
				
			||||||
    Query(Accounts) filter(_.mailAddress is mailAddress.bind) firstOption
 | 
					    Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
 | 
					  def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] =
 | 
				
			||||||
 | 
					    if(includeRemoved){
 | 
				
			||||||
 | 
					      Accounts sortBy(_.userName) list
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      Accounts filter (_.removed === false.bind) sortBy(_.userName) list
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
 | 
					  def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
 | 
				
			||||||
 | 
					                   (implicit s: Session): Unit =
 | 
				
			||||||
    Accounts insert Account(
 | 
					    Accounts insert Account(
 | 
				
			||||||
      userName       = userName,
 | 
					      userName       = userName,
 | 
				
			||||||
      password       = password,
 | 
					      password       = password,
 | 
				
			||||||
@@ -71,12 +100,13 @@ trait AccountService {
 | 
				
			|||||||
      updatedDate    = currentDate,
 | 
					      updatedDate    = currentDate,
 | 
				
			||||||
      lastLoginDate  = None,
 | 
					      lastLoginDate  = None,
 | 
				
			||||||
      image          = None,
 | 
					      image          = None,
 | 
				
			||||||
      isGroupAccount = false)
 | 
					      isGroupAccount = false,
 | 
				
			||||||
 | 
					      isRemoved      = false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateAccount(account: Account): Unit = 
 | 
					  def updateAccount(account: Account)(implicit s: Session): Unit =
 | 
				
			||||||
    Accounts
 | 
					    Accounts
 | 
				
			||||||
      .filter { a => a.userName is account.userName.bind }
 | 
					      .filter { a =>  a.userName === account.userName.bind }
 | 
				
			||||||
      .map    { a => a.password ~ a.fullName ~ 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.?, a.removed) }
 | 
				
			||||||
      .update (
 | 
					      .update (
 | 
				
			||||||
        account.password,
 | 
					        account.password,
 | 
				
			||||||
        account.fullName,
 | 
					        account.fullName,
 | 
				
			||||||
@@ -85,15 +115,16 @@ trait AccountService {
 | 
				
			|||||||
        account.url,
 | 
					        account.url,
 | 
				
			||||||
        account.registeredDate,
 | 
					        account.registeredDate,
 | 
				
			||||||
        currentDate,
 | 
					        currentDate,
 | 
				
			||||||
        account.lastLoginDate)
 | 
					        account.lastLoginDate,
 | 
				
			||||||
 | 
					        account.isRemoved)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateAvatarImage(userName: String, image: Option[String]): Unit =
 | 
					  def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit =
 | 
				
			||||||
    Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image)
 | 
					    Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateLastLoginDate(userName: String): Unit =
 | 
					  def updateLastLoginDate(userName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
 | 
					    Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createGroup(groupName: String, url: Option[String]): Unit =
 | 
					  def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit =
 | 
				
			||||||
    Accounts insert Account(
 | 
					    Accounts insert Account(
 | 
				
			||||||
      userName       = groupName,
 | 
					      userName       = groupName,
 | 
				
			||||||
      password       = "",
 | 
					      password       = "",
 | 
				
			||||||
@@ -105,32 +136,38 @@ trait AccountService {
 | 
				
			|||||||
      updatedDate    = currentDate,
 | 
					      updatedDate    = currentDate,
 | 
				
			||||||
      lastLoginDate  = None,
 | 
					      lastLoginDate  = None,
 | 
				
			||||||
      image          = None,
 | 
					      image          = None,
 | 
				
			||||||
      isGroupAccount = true)
 | 
					      isGroupAccount = true,
 | 
				
			||||||
 | 
					      isRemoved      = false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateGroup(groupName: String, url: Option[String]): Unit =
 | 
					  def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit =
 | 
				
			||||||
    Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
 | 
					    Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateGroupMembers(groupName: String, members: List[String]): Unit = {
 | 
					  def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = {
 | 
				
			||||||
    Query(GroupMembers).filter(_.groupName is groupName.bind).delete
 | 
					    GroupMembers.filter(_.groupName === groupName.bind).delete
 | 
				
			||||||
    members.foreach { userName =>
 | 
					    members.foreach { case (userName, isManager) =>
 | 
				
			||||||
      GroupMembers insert GroupMember (groupName, userName)
 | 
					      GroupMembers insert GroupMember (groupName, userName, isManager)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getGroupMembers(groupName: String): List[String] =
 | 
					  def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] =
 | 
				
			||||||
    Query(GroupMembers)
 | 
					    GroupMembers
 | 
				
			||||||
      .filter(_.groupName is groupName.bind)
 | 
					      .filter(_.groupName === groupName.bind)
 | 
				
			||||||
      .sortBy(_.userName)
 | 
					      .sortBy(_.userName)
 | 
				
			||||||
      .map(_.userName)
 | 
					 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getGroupsByUserName(userName: String): List[String] =
 | 
					  def getGroupsByUserName(userName: String)(implicit s: Session): List[String] =
 | 
				
			||||||
    Query(GroupMembers)
 | 
					    GroupMembers
 | 
				
			||||||
      .filter(_.userName is userName.bind)
 | 
					      .filter(_.userName === userName.bind)
 | 
				
			||||||
      .sortBy(_.groupName)
 | 
					      .sortBy(_.groupName)
 | 
				
			||||||
      .map(_.groupName)
 | 
					      .map(_.groupName)
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
 | 
				
			||||||
 | 
					    GroupMembers.filter(_.userName === userName.bind).delete
 | 
				
			||||||
 | 
					    Collaborators.filter(_.collaboratorName === userName.bind).delete
 | 
				
			||||||
 | 
					    Repositories.filter(_.userName === userName.bind).delete
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object AccountService extends AccountService
 | 
					object AccountService extends AccountService
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,19 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import model._
 | 
					import model.Profile._
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import profile.simple._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import model.Activity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait ActivityService {
 | 
					trait ActivityService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
 | 
					  def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
 | 
				
			||||||
    Activities
 | 
					    Activities
 | 
				
			||||||
      .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
 | 
					      .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
 | 
				
			||||||
      .filter { case (t1, t2) =>
 | 
					      .filter { case (t1, t2) =>
 | 
				
			||||||
        if(isPublic){
 | 
					        if(isPublic){
 | 
				
			||||||
          (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
 | 
					          (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          (t1.activityUserName is activityUserName.bind)
 | 
					          (t1.activityUserName === activityUserName.bind)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .sortBy { case (t1, t2) => t1.activityId desc }
 | 
					      .sortBy { case (t1, t2) => t1.activityId desc }
 | 
				
			||||||
@@ -21,151 +21,159 @@ trait ActivityService {
 | 
				
			|||||||
      .take(30)
 | 
					      .take(30)
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getRecentActivities(): List[Activity] =
 | 
					  def getRecentActivities()(implicit s: Session): List[Activity] =
 | 
				
			||||||
    Activities
 | 
					    Activities
 | 
				
			||||||
      .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
 | 
					      .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
 | 
				
			||||||
      .filter { case (t1, t2) => t2.isPrivate is false.bind }
 | 
					      .filter { case (t1, t2) =>  t2.isPrivate === false.bind }
 | 
				
			||||||
      .sortBy { case (t1, t2) => t1.activityId desc }
 | 
					      .sortBy { case (t1, t2) => t1.activityId desc }
 | 
				
			||||||
      .map    { case (t1, t2) => t1 }
 | 
					      .map    { case (t1, t2) => t1 }
 | 
				
			||||||
      .take(30)
 | 
					      .take(30)
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] =
 | 
				
			||||||
 | 
					    Activities
 | 
				
			||||||
 | 
					      .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
 | 
				
			||||||
 | 
					      .filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) }
 | 
				
			||||||
 | 
					      .sortBy { case (t1, t2) => t1.activityId desc }
 | 
				
			||||||
 | 
					      .map    { case (t1, t2) => t1 }
 | 
				
			||||||
 | 
					      .take(30)
 | 
				
			||||||
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String): Unit =
 | 
					  def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                    (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "create_repository",
 | 
					      "create_repository",
 | 
				
			||||||
      s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
 | 
					  def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                               (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "open_issue",
 | 
					      "open_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(title), 
 | 
					      Some(title), 
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
 | 
					  def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                              (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "close_issue",
 | 
					      "close_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(title),
 | 
					      Some(title),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
 | 
					  def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                    (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "close_issue",
 | 
					      "close_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(title),
 | 
					      Some(title),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
 | 
					  def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                               (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "reopen_issue",
 | 
					      "reopen_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(title),
 | 
					      Some(title),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
 | 
					  def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "comment_issue",
 | 
					      "comment_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(cut(comment, 200)),
 | 
					      Some(cut(comment, 200)),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
 | 
					  def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                      (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "comment_issue",
 | 
					      "comment_issue",
 | 
				
			||||||
      s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(cut(comment, 200)),
 | 
					      Some(cut(comment, 200)),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
 | 
					  def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                  (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "create_wiki",
 | 
					      "create_wiki",
 | 
				
			||||||
      s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
 | 
					      s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki",
 | 
				
			||||||
      Some(pageName),
 | 
					      Some(pageName),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
 | 
					  def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "edit_wiki",
 | 
					      "edit_wiki",
 | 
				
			||||||
      s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
 | 
					      s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
 | 
				
			||||||
      Some(pageName + ":" + commitId),
 | 
					      Some(pageName + ":" + commitId),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
 | 
					  def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
 | 
				
			||||||
      branchName: String, commits: List[util.JGitUtil.CommitInfo]) =
 | 
					      branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "push",
 | 
					      "push",
 | 
				
			||||||
      s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
 | 
					      Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, 
 | 
					  def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, 
 | 
				
			||||||
      tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
 | 
					      tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "create_tag",
 | 
					      "create_tag",
 | 
				
			||||||
      s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
 | 
					  def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String,
 | 
				
			||||||
                              tagName: String, commits: List[util.JGitUtil.CommitInfo]) =
 | 
					                              tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit =
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "delete_tag",
 | 
					      "delete_tag",
 | 
				
			||||||
      s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
 | 
					  def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "create_branch",
 | 
					      "create_branch",
 | 
				
			||||||
      s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
 | 
					  def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                                (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "delete_branch",
 | 
					      "delete_branch",
 | 
				
			||||||
      s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
 | 
					  def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "fork",
 | 
					      "fork",
 | 
				
			||||||
      s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
 | 
					      s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
 | 
				
			||||||
      None,
 | 
					      None,
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
 | 
					  def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                               (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "open_pullreq",
 | 
					      "open_pullreq",
 | 
				
			||||||
      s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(title),
 | 
					      Some(title),
 | 
				
			||||||
      currentDate)
 | 
					      currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
 | 
					  def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String)
 | 
				
			||||||
    Activities.autoInc insert(userName, repositoryName, activityUserName,
 | 
					                         (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Activities insert Activity(userName, repositoryName, activityUserName,
 | 
				
			||||||
      "merge_pullreq",
 | 
					      "merge_pullreq",
 | 
				
			||||||
      s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
					      s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
 | 
				
			||||||
      Some(message),
 | 
					      Some(message),
 | 
				
			||||||
      currentDate)
 | 
					      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
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  private def cut(value: String, length: Int): String =
 | 
					  private def cut(value: String, length: Int): String =
 | 
				
			||||||
    if(value.length > length) value.substring(0, length) + "..." else value
 | 
					    if(value.length > length) value.substring(0, length) + "..." else value
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +1,33 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					 | 
				
			||||||
import Database.threadLocalSession
 | 
					 | 
				
			||||||
import scala.slick.jdbc.{StaticQuery => Q}
 | 
					import scala.slick.jdbc.{StaticQuery => Q}
 | 
				
			||||||
import Q.interpolation
 | 
					import Q.interpolation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import model._
 | 
					import model.Profile._
 | 
				
			||||||
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.{Issue, IssueComment, IssueLabel, Label}
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import util.StringUtil._
 | 
					import util.StringUtil._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait IssuesService {
 | 
					trait IssuesService {
 | 
				
			||||||
  import IssuesService._
 | 
					  import IssuesService._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getIssue(owner: String, repository: String, issueId: String) =
 | 
					  def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
 | 
				
			||||||
    if (issueId forall (_.isDigit))
 | 
					    if (issueId forall (_.isDigit))
 | 
				
			||||||
      Query(Issues) filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
 | 
					      Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
 | 
				
			||||||
    else None
 | 
					    else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getComments(owner: String, repository: String, issueId: Int) =
 | 
					  def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
 | 
				
			||||||
    Query(IssueComments) filter (_.byIssue(owner, repository, issueId)) list
 | 
					    IssueComments filter (_.byIssue(owner, repository, issueId)) list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getComment(owner: String, repository: String, commentId: String) =
 | 
					  def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
 | 
				
			||||||
    if (commentId forall (_.isDigit))
 | 
					    if (commentId forall (_.isDigit))
 | 
				
			||||||
      Query(IssueComments) filter { t =>
 | 
					      IssueComments filter { t =>
 | 
				
			||||||
        t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
 | 
					        t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
 | 
				
			||||||
      } firstOption
 | 
					      } firstOption
 | 
				
			||||||
    else None
 | 
					    else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getIssueLabels(owner: String, repository: String, issueId: Int) =
 | 
					  def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) =
 | 
				
			||||||
    IssueLabels
 | 
					    IssueLabels
 | 
				
			||||||
      .innerJoin(Labels).on { (t1, t2) =>
 | 
					      .innerJoin(Labels).on { (t1, t2) =>
 | 
				
			||||||
        t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
 | 
					        t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
 | 
				
			||||||
@@ -36,8 +36,8 @@ trait IssuesService {
 | 
				
			|||||||
      .map    ( _._2 )
 | 
					      .map    ( _._2 )
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
 | 
					  def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
 | 
				
			||||||
    Query(IssueLabels) filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
 | 
					    IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the count of the search result against  issues.
 | 
					   * Returns the count of the search result against  issues.
 | 
				
			||||||
@@ -49,8 +49,9 @@ trait IssuesService {
 | 
				
			|||||||
   * @return the count of the search result
 | 
					   * @return the count of the search result
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
					  def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
				
			||||||
                 repos: (String, String)*): Int =
 | 
					                 repos: (String, String)*)(implicit s: Session): Int =
 | 
				
			||||||
    Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
 | 
					    Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the Map which contains issue count for each labels.
 | 
					   * Returns the Map which contains issue count for each labels.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
@@ -61,7 +62,7 @@ trait IssuesService {
 | 
				
			|||||||
   * @return the Map which contains issue count for each labels (key is label name, value is issue count)
 | 
					   * @return the Map which contains issue count for each labels (key is label name, value is issue count)
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
 | 
					  def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
 | 
				
			||||||
                              filterUser: Map[String, String]): Map[String, Int] = {
 | 
					                              filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
 | 
					    searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
 | 
				
			||||||
      .innerJoin(IssueLabels).on { (t1, t2) =>
 | 
					      .innerJoin(IssueLabels).on { (t1, t2) =>
 | 
				
			||||||
@@ -74,7 +75,7 @@ trait IssuesService {
 | 
				
			|||||||
        t3.labelName
 | 
					        t3.labelName
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .map { case (labelName, t) =>
 | 
					      .map { case (labelName, t) =>
 | 
				
			||||||
        labelName ~ t.length
 | 
					        labelName -> t.length
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .toMap
 | 
					      .toMap
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -90,13 +91,13 @@ trait IssuesService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def countIssueGroupByRepository(
 | 
					  def countIssueGroupByRepository(
 | 
				
			||||||
      condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
					      condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
				
			||||||
      repos: (String, String)*): List[(String, String, Int)] = {
 | 
					      repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
 | 
				
			||||||
    searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
 | 
					    searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
 | 
				
			||||||
      .groupBy { t =>
 | 
					      .groupBy { t =>
 | 
				
			||||||
        t.userName ~ t.repositoryName
 | 
					        t.userName -> t.repositoryName
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .map { case (repo, t) =>
 | 
					      .map { case (repo, t) =>
 | 
				
			||||||
        repo ~ t.length
 | 
					        (repo._1, repo._2, t.length)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .sortBy(_._3 desc)
 | 
					      .sortBy(_._3 desc)
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
@@ -114,21 +115,16 @@ trait IssuesService {
 | 
				
			|||||||
   * @return the search result (list of tuples which contain issue, labels and comment count)
 | 
					   * @return the search result (list of tuples which contain issue, labels and comment count)
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
					  def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
 | 
				
			||||||
                  offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
 | 
					                  offset: Int, limit: Int, repos: (String, String)*)
 | 
				
			||||||
 | 
					                 (implicit s: Session): List[(Issue, List[Label], Int)] = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // get issues and comment count and labels
 | 
					    // get issues and comment count and labels
 | 
				
			||||||
    searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
 | 
					    searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
 | 
				
			||||||
        .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
 | 
					        .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) }
 | 
					        .sortBy { case (t1, t2) =>
 | 
				
			||||||
        .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.?)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .sortBy(_._4)	// labelName
 | 
					 | 
				
			||||||
        .sortBy { case (t1, commentCount, _,_,_) =>
 | 
					 | 
				
			||||||
          (condition.sort match {
 | 
					          (condition.sort match {
 | 
				
			||||||
            case "created"  => t1.registeredDate
 | 
					            case "created"  => t1.registeredDate
 | 
				
			||||||
            case "comments" => commentCount
 | 
					            case "comments" => t2.commentCount
 | 
				
			||||||
            case "updated"  => t1.updatedDate
 | 
					            case "updated"  => t1.updatedDate
 | 
				
			||||||
          }) match {
 | 
					          }) match {
 | 
				
			||||||
            case sort => condition.direction match {
 | 
					            case sort => condition.direction match {
 | 
				
			||||||
@@ -138,6 +134,11 @@ trait IssuesService {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .drop(offset).take(limit)
 | 
					        .drop(offset).take(limit)
 | 
				
			||||||
 | 
					        .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.?)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        .list
 | 
					        .list
 | 
				
			||||||
        .splitWith { (c1, c2) =>
 | 
					        .splitWith { (c1, c2) =>
 | 
				
			||||||
          c1._1.userName == c2._1.userName &&
 | 
					          c1._1.userName == c2._1.userName &&
 | 
				
			||||||
@@ -158,20 +159,20 @@ trait IssuesService {
 | 
				
			|||||||
   * Assembles query for conditional issue searching.
 | 
					   * Assembles query for conditional issue searching.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
 | 
					  private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
 | 
				
			||||||
                               filterUser: Map[String, String], onlyPullRequest: Boolean) =
 | 
					                               filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
 | 
				
			||||||
    Query(Issues) filter { t1 =>
 | 
					    Issues filter { t1 =>
 | 
				
			||||||
      condition.repo
 | 
					      condition.repo
 | 
				
			||||||
          .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
 | 
					          .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
 | 
				
			||||||
          .getOrElse (repos)
 | 
					          .getOrElse (repos)
 | 
				
			||||||
          .map { case (owner, repository) => t1.byRepository(owner, repository) }
 | 
					          .map { case (owner, repository) => t1.byRepository(owner, repository) }
 | 
				
			||||||
          .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
 | 
					          .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
 | 
				
			||||||
      (t1.closed           is (condition.state == "closed").bind) &&
 | 
					      (t1.closed           === (condition.state == "closed").bind) &&
 | 
				
			||||||
      (t1.milestoneId      is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
 | 
					      (t1.milestoneId      === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
 | 
				
			||||||
      (t1.milestoneId      isNull, condition.milestoneId == Some(None)) &&
 | 
					      (t1.milestoneId.?    isEmpty, condition.milestoneId == Some(None)) &&
 | 
				
			||||||
      (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
 | 
					      (t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
 | 
				
			||||||
      (t1.openedUserName   is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
 | 
					      (t1.openedUserName   === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
 | 
				
			||||||
      (t1.openedUserName   isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
 | 
					      (t1.openedUserName   =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
 | 
				
			||||||
      (t1.pullRequest      is true.bind, onlyPullRequest) &&
 | 
					      (t1.pullRequest      === true.bind, onlyPullRequest) &&
 | 
				
			||||||
      (IssueLabels filter { t2 =>
 | 
					      (IssueLabels filter { t2 =>
 | 
				
			||||||
        (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
 | 
					        (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
 | 
				
			||||||
        (t2.labelId in
 | 
					        (t2.labelId in
 | 
				
			||||||
@@ -183,7 +184,8 @@ trait IssuesService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
 | 
					  def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
 | 
				
			||||||
                  assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
 | 
					                  assignedUserName: Option[String], milestoneId: Option[Int],
 | 
				
			||||||
 | 
					                  isPullRequest: Boolean = false)(implicit s: Session) =
 | 
				
			||||||
    // next id number
 | 
					    // next id number
 | 
				
			||||||
    sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
 | 
					    sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
 | 
				
			||||||
        .firstOption.filter { id =>
 | 
					        .firstOption.filter { id =>
 | 
				
			||||||
@@ -208,55 +210,57 @@ trait IssuesService {
 | 
				
			|||||||
        .update (id) > 0
 | 
					        .update (id) > 0
 | 
				
			||||||
    } get
 | 
					    } get
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
 | 
					  def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
 | 
				
			||||||
    IssueLabels insert (IssueLabel(owner, repository, issueId, labelId))
 | 
					    IssueLabels insert IssueLabel(owner, repository, issueId, labelId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int) =
 | 
					  def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) =
 | 
				
			||||||
    IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
 | 
					    IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createComment(owner: String, repository: String, loginUser: String,
 | 
					  def createComment(owner: String, repository: String, loginUser: String,
 | 
				
			||||||
      issueId: Int, content: String, action: String) =
 | 
					      issueId: Int, content: String, action: String)(implicit s: Session): Int =
 | 
				
			||||||
    IssueComments.autoInc insert (
 | 
					    IssueComments.autoInc insert IssueComment(
 | 
				
			||||||
        owner,
 | 
					        userName          = owner,
 | 
				
			||||||
        repository,
 | 
					        repositoryName    = repository,
 | 
				
			||||||
        issueId,
 | 
					        issueId           = issueId,
 | 
				
			||||||
        action,
 | 
					        action            = action,
 | 
				
			||||||
        loginUser,
 | 
					        commentedUserName = loginUser,
 | 
				
			||||||
        content,
 | 
					        content           = content,
 | 
				
			||||||
        currentDate,
 | 
					        registeredDate    = currentDate,
 | 
				
			||||||
        currentDate)
 | 
					        updatedDate       = currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateIssue(owner: String, repository: String, issueId: Int,
 | 
					  def updateIssue(owner: String, repository: String, issueId: Int,
 | 
				
			||||||
      title: String, content: Option[String]) =
 | 
					      title: String, content: Option[String])(implicit s: Session) =
 | 
				
			||||||
    Issues
 | 
					    Issues
 | 
				
			||||||
      .filter (_.byPrimaryKey(owner, repository, issueId))
 | 
					      .filter (_.byPrimaryKey(owner, repository, issueId))
 | 
				
			||||||
      .map { t =>
 | 
					      .map { t =>
 | 
				
			||||||
        t.title ~ t.content.? ~ t.updatedDate
 | 
					        (t.title, t.content.?, t.updatedDate)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .update (title, content, currentDate)
 | 
					      .update (title, content, currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String]) =
 | 
					  def updateAssignedUserName(owner: String, repository: String, issueId: Int,
 | 
				
			||||||
 | 
					                             assignedUserName: Option[String])(implicit s: Session) =
 | 
				
			||||||
    Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
 | 
					    Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int]) =
 | 
					  def updateMilestoneId(owner: String, repository: String, issueId: Int,
 | 
				
			||||||
 | 
					                        milestoneId: Option[Int])(implicit s: Session) =
 | 
				
			||||||
    Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
 | 
					    Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateComment(commentId: Int, content: String) =
 | 
					  def updateComment(commentId: Int, content: String)(implicit s: Session) =
 | 
				
			||||||
    IssueComments
 | 
					    IssueComments
 | 
				
			||||||
      .filter (_.byPrimaryKey(commentId))
 | 
					      .filter (_.byPrimaryKey(commentId))
 | 
				
			||||||
      .map { t =>
 | 
					      .map { t =>
 | 
				
			||||||
        t.content ~ t.updatedDate
 | 
					        t.content -> t.updatedDate
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .update (content, currentDate)
 | 
					      .update (content, currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteComment(commentId: Int) =
 | 
					  def deleteComment(commentId: Int)(implicit s: Session) =
 | 
				
			||||||
    IssueComments filter (_.byPrimaryKey(commentId)) delete
 | 
					    IssueComments filter (_.byPrimaryKey(commentId)) delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean) =
 | 
					  def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) =
 | 
				
			||||||
    Issues
 | 
					    Issues
 | 
				
			||||||
      .filter (_.byPrimaryKey(owner, repository, issueId))
 | 
					      .filter (_.byPrimaryKey(owner, repository, issueId))
 | 
				
			||||||
      .map { t =>
 | 
					      .map { t =>
 | 
				
			||||||
        t.closed ~ t.updatedDate
 | 
					        t.closed -> t.updatedDate
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .update (closed, currentDate)
 | 
					      .update (closed, currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -268,8 +272,9 @@ trait IssuesService {
 | 
				
			|||||||
   * @param query the keywords separated by whitespace.
 | 
					   * @param query the keywords separated by whitespace.
 | 
				
			||||||
   * @return issues with comment count and matched content of issue or comment
 | 
					   * @return issues with comment count and matched content of issue or comment
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
 | 
					  def searchIssuesByKeyword(owner: String, repository: String, query: String)
 | 
				
			||||||
    import scala.slick.driver.H2Driver.likeEncode
 | 
					                           (implicit s: Session): List[(Issue, Int, String)] = {
 | 
				
			||||||
 | 
					    import slick.driver.JdbcDriver.likeEncode
 | 
				
			||||||
    val keywords = splitWords(query.toLowerCase)
 | 
					    val keywords = splitWords(query.toLowerCase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Search Issue
 | 
					    // Search Issue
 | 
				
			||||||
@@ -305,7 +310,7 @@ trait IssuesService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    issues.union(comments).sortBy { case (issue, commentId, _, _) =>
 | 
					    issues.union(comments).sortBy { case (issue, commentId, _, _) =>
 | 
				
			||||||
      issue.issueId ~ commentId
 | 
					      issue.issueId -> commentId
 | 
				
			||||||
    }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
 | 
					    }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
 | 
				
			||||||
      issue1.issueId == issue2.issueId
 | 
					      issue1.issueId == issue2.issueId
 | 
				
			||||||
    }.map { _.head match {
 | 
					    }.map { _.head match {
 | 
				
			||||||
@@ -314,6 +319,14 @@ trait IssuesService {
 | 
				
			|||||||
    }.toList
 | 
					    }.toList
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = {
 | 
				
			||||||
 | 
					    extractCloseId(message).foreach { issueId =>
 | 
				
			||||||
 | 
					      for(issue <- getIssue(owner, repository, issueId) if !issue.closed){
 | 
				
			||||||
 | 
					        createComment(owner, repository, userName, issue.issueId, "Close", "close")
 | 
				
			||||||
 | 
					        updateClosed(owner, repository, issue.issueId, true)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object IssuesService {
 | 
					object IssuesService {
 | 
				
			||||||
@@ -331,7 +344,7 @@ object IssuesService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def toURL: String =
 | 
					    def toURL: String =
 | 
				
			||||||
      "?" + List(
 | 
					      "?" + List(
 | 
				
			||||||
        if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(" "))),
 | 
					        if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
 | 
				
			||||||
        milestoneId.map { id => "milestone=" + (id match {
 | 
					        milestoneId.map { id => "milestone=" + (id match {
 | 
				
			||||||
          case Some(x) => x.toString
 | 
					          case Some(x) => x.toString
 | 
				
			||||||
          case None    => "none"
 | 
					          case None    => "none"
 | 
				
			||||||
@@ -352,7 +365,7 @@ object IssuesService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def apply(request: HttpServletRequest): IssueSearchCondition =
 | 
					    def apply(request: HttpServletRequest): IssueSearchCondition =
 | 
				
			||||||
      IssueSearchCondition(
 | 
					      IssueSearchCondition(
 | 
				
			||||||
        param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
 | 
					        param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
 | 
				
			||||||
        param(request, "milestone").map{
 | 
					        param(request, "milestone").map{
 | 
				
			||||||
          case "none" => None
 | 
					          case "none" => None
 | 
				
			||||||
          case x      => x.toIntOpt
 | 
					          case x      => x.toIntOpt
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,32 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import model.Profile._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.Label
 | 
				
			||||||
import model._
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait LabelsService {
 | 
					trait LabelsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getLabels(owner: String, repository: String): List[Label] =
 | 
					  def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] =
 | 
				
			||||||
    Query(Labels).filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
 | 
					    Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getLabel(owner: String, repository: String, labelId: Int): Option[Label] =
 | 
					  def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
 | 
				
			||||||
    Query(Labels).filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
 | 
					    Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createLabel(owner: String, repository: String, labelName: String, color: String): Unit =
 | 
					  def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Labels.ins insert (owner, repository, labelName, color)
 | 
					    Labels insert Label(
 | 
				
			||||||
 | 
					      userName       = owner,
 | 
				
			||||||
 | 
					      repositoryName = repository,
 | 
				
			||||||
 | 
					      labelName      = labelName,
 | 
				
			||||||
 | 
					      color          = color
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String): Unit =
 | 
					  def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String)
 | 
				
			||||||
    Labels.filter(_.byPrimaryKey(owner, repository, labelId)).map(t => t.labelName ~ t.color)
 | 
					                 (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Labels.filter(_.byPrimaryKey(owner, repository, labelId))
 | 
				
			||||||
 | 
					          .map(t => t.labelName -> t.color)
 | 
				
			||||||
          .update(labelName, color)
 | 
					          .update(labelName, color)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteLabel(owner: String, repository: String, labelId: Int): Unit = {
 | 
					  def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = {
 | 
				
			||||||
    IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
 | 
					    IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete
 | 
				
			||||||
    Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
 | 
					    Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,39 +1,49 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import model.Profile._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.Milestone
 | 
				
			||||||
import model._
 | 
					// TODO [Slick 2.0]NOT import directly?
 | 
				
			||||||
 | 
					import model.Profile.dateColumnType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait MilestonesService {
 | 
					trait MilestonesService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createMilestone(owner: String, repository: String, title: String, description: Option[String],
 | 
					  def createMilestone(owner: String, repository: String, title: String, description: Option[String],
 | 
				
			||||||
                      dueDate: Option[java.util.Date]): Unit =
 | 
					                      dueDate: Option[java.util.Date])(implicit s: Session): Unit =
 | 
				
			||||||
    Milestones.ins insert (owner, repository, title, description, dueDate, None)
 | 
					    Milestones insert Milestone(
 | 
				
			||||||
 | 
					      userName       = owner,
 | 
				
			||||||
 | 
					      repositoryName = repository,
 | 
				
			||||||
 | 
					      title          = title,
 | 
				
			||||||
 | 
					      description    = description,
 | 
				
			||||||
 | 
					      dueDate        = dueDate,
 | 
				
			||||||
 | 
					      closedDate     = None
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def updateMilestone(milestone: Milestone): Unit =
 | 
					  def updateMilestone(milestone: Milestone)(implicit s: Session): Unit =
 | 
				
			||||||
    Milestones
 | 
					    Milestones
 | 
				
			||||||
      .filter (t =>  t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
 | 
					      .filter (t =>  t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId))
 | 
				
			||||||
      .map    (t => t.title ~ t.description.? ~ t.dueDate.? ~ t.closedDate.?)
 | 
					      .map    (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?))
 | 
				
			||||||
      .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
 | 
					      .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def openMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = None))
 | 
					  def openMilestone(milestone: Milestone)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    updateMilestone(milestone.copy(closedDate = None))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def closeMilestone(milestone: Milestone): Unit = updateMilestone(milestone.copy(closedDate = Some(currentDate)))
 | 
					  def closeMilestone(milestone: Milestone)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    updateMilestone(milestone.copy(closedDate = Some(currentDate)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteMilestone(owner: String, repository: String, milestoneId: Int): Unit = {
 | 
					  def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = {
 | 
				
			||||||
    Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
 | 
					    Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None)
 | 
				
			||||||
    Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
 | 
					    Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] =
 | 
					  def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] =
 | 
				
			||||||
    Query(Milestones).filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
 | 
					    Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getMilestonesWithIssueCount(owner: String, repository: String): List[(Milestone, Int, Int)] = {
 | 
					  def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
 | 
				
			||||||
    val counts = Issues
 | 
					    val counts = Issues
 | 
				
			||||||
      .filter  { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) }
 | 
					      .filter  { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) }
 | 
				
			||||||
      .groupBy { t => t.milestoneId ~ t.closed }
 | 
					      .groupBy { t => t.milestoneId -> t.closed }
 | 
				
			||||||
      .map     { case (t1, t2) => (t1._1 ~ t1._2) -> t2.length }
 | 
					      .map     { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
 | 
				
			||||||
      .toMap
 | 
					      .toMap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getMilestones(owner, repository).map { milestone =>
 | 
					    getMilestones(owner, repository).map { milestone =>
 | 
				
			||||||
@@ -41,6 +51,7 @@ trait MilestonesService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getMilestones(owner: String, repository: String): List[Milestone] =
 | 
					  def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
 | 
				
			||||||
    Query(Milestones).filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
 | 
					    Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								src/main/scala/service/PluginService.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/main/scala/service/PluginService.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import model.Profile._
 | 
				
			||||||
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.Plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait PluginService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getPlugins()(implicit s: Session): List[Plugin] =
 | 
				
			||||||
 | 
					    Plugins.sortBy(_.pluginId).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def registerPlugin(plugin: Plugin)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Plugins.insert(plugin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def updatePlugin(plugin: Plugin)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def deletePlugin(pluginId: String)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    Plugins.filter(_.pluginId === pluginId.bind).delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] =
 | 
				
			||||||
 | 
					    Plugins.filter(_.pluginId === pluginId.bind).firstOption
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,38 +1,45 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import model.Profile._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import profile.simple._
 | 
				
			||||||
import model._
 | 
					import model.{PullRequest, Issue}
 | 
				
			||||||
import util.ControlUtil._
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait PullRequestService { self: IssuesService =>
 | 
					trait PullRequestService { self: IssuesService =>
 | 
				
			||||||
  import PullRequestService._
 | 
					  import PullRequestService._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
 | 
					  def getPullRequest(owner: String, repository: String, issueId: Int)
 | 
				
			||||||
 | 
					                    (implicit s: Session): Option[(Issue, PullRequest)] =
 | 
				
			||||||
    getIssue(owner, repository, issueId.toString).flatMap{ issue =>
 | 
					    getIssue(owner, repository, issueId.toString).flatMap{ issue =>
 | 
				
			||||||
      Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
 | 
					      PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
 | 
				
			||||||
        pullreq => (issue, pullreq)
 | 
					        pullreq => (issue, pullreq)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
 | 
					  def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String)
 | 
				
			||||||
    Query(PullRequests)
 | 
					                    (implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    PullRequests.filter(_.byPrimaryKey(owner, repository, issueId))
 | 
				
			||||||
 | 
					                .map(pr => pr.commitIdTo -> pr.commitIdFrom)
 | 
				
			||||||
 | 
					                .update((commitIdTo, commitIdFrom))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])
 | 
				
			||||||
 | 
					                                    (implicit s: Session): List[PullRequestCount] =
 | 
				
			||||||
 | 
					    PullRequests
 | 
				
			||||||
      .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
 | 
					      .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
 | 
				
			||||||
      .filter { case (t1, t2) =>
 | 
					      .filter { case (t1, t2) =>
 | 
				
			||||||
        (t2.closed         is closed.bind) &&
 | 
					        (t2.closed         === closed.bind) &&
 | 
				
			||||||
        (t1.userName       is owner.bind) &&
 | 
					        (t1.userName       === owner.get.bind, owner.isDefined) &&
 | 
				
			||||||
        (t1.repositoryName is repository.get.bind, repository.isDefined)
 | 
					        (t1.repositoryName === repository.get.bind, repository.isDefined)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .groupBy { case (t1, t2) => t2.openedUserName }
 | 
					      .groupBy { case (t1, t2) => t2.openedUserName }
 | 
				
			||||||
      .map { case (userName, t) => userName ~ t.length }
 | 
					      .map { case (userName, t) => userName -> t.length }
 | 
				
			||||||
      .sortBy(_._2 desc)
 | 
					      .sortBy(_._2 desc)
 | 
				
			||||||
      .list
 | 
					      .list
 | 
				
			||||||
      .map { x => PullRequestCount(x._1, x._2) }
 | 
					      .map { x => PullRequestCount(x._1, x._2) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
 | 
					  def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
 | 
				
			||||||
        originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
 | 
					        originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
 | 
				
			||||||
        commitIdFrom: String, commitIdTo: String): Unit =
 | 
					        commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =
 | 
				
			||||||
    PullRequests insert (PullRequest(
 | 
					    PullRequests insert PullRequest(
 | 
				
			||||||
      originUserName,
 | 
					      originUserName,
 | 
				
			||||||
      originRepositoryName,
 | 
					      originRepositoryName,
 | 
				
			||||||
      issueId,
 | 
					      issueId,
 | 
				
			||||||
@@ -41,7 +48,20 @@ trait PullRequestService { self: IssuesService =>
 | 
				
			|||||||
      requestRepositoryName,
 | 
					      requestRepositoryName,
 | 
				
			||||||
      requestBranch,
 | 
					      requestBranch,
 | 
				
			||||||
      commitIdFrom,
 | 
					      commitIdFrom,
 | 
				
			||||||
      commitIdTo))
 | 
					      commitIdTo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
 | 
				
			||||||
 | 
					                              (implicit s: Session): List[PullRequest] =
 | 
				
			||||||
 | 
					    PullRequests
 | 
				
			||||||
 | 
					      .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
 | 
				
			||||||
 | 
					      .filter { case (t1, t2) =>
 | 
				
			||||||
 | 
					        (t1.requestUserName       === userName.bind) &&
 | 
				
			||||||
 | 
					        (t1.requestRepositoryName === repositoryName.bind) &&
 | 
				
			||||||
 | 
					        (t1.requestBranch         === branch.bind) &&
 | 
				
			||||||
 | 
					        (t2.closed                === closed.bind)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .map { case (t1, t2) => t1 }
 | 
				
			||||||
 | 
					      .list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,21 +3,20 @@ package service
 | 
				
			|||||||
import util.{FileUtil, StringUtil, JGitUtil}
 | 
					import util.{FileUtil, StringUtil, JGitUtil}
 | 
				
			||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import model.Issue
 | 
					 | 
				
			||||||
import org.eclipse.jgit.revwalk.RevWalk
 | 
					import org.eclipse.jgit.revwalk.RevWalk
 | 
				
			||||||
import org.eclipse.jgit.treewalk.TreeWalk
 | 
					import org.eclipse.jgit.treewalk.TreeWalk
 | 
				
			||||||
import scala.collection.mutable.ListBuffer
 | 
					 | 
				
			||||||
import org.eclipse.jgit.lib.FileMode
 | 
					import org.eclipse.jgit.lib.FileMode
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
 | 
					import model.Profile._
 | 
				
			||||||
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait
 | 
					trait RepositorySearchService { self: IssuesService =>
 | 
				
			||||||
RepositorySearchService { self: IssuesService =>
 | 
					 | 
				
			||||||
  import RepositorySearchService._
 | 
					  import RepositorySearchService._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def countIssues(owner: String, repository: String, query: String): Int =
 | 
					  def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int =
 | 
				
			||||||
    searchIssuesByKeyword(owner, repository, query).length
 | 
					    searchIssuesByKeyword(owner, repository, query).length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
 | 
					  def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] =
 | 
				
			||||||
    searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
 | 
					    searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
 | 
				
			||||||
      IssueSearchResult(
 | 
					      IssueSearchResult(
 | 
				
			||||||
        issue.issueId,
 | 
					        issue.issueId,
 | 
				
			||||||
@@ -39,7 +38,7 @@ RepositorySearchService { self: IssuesService =>
 | 
				
			|||||||
        Nil
 | 
					        Nil
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        val files = searchRepositoryFiles(git, query)
 | 
					        val files = searchRepositoryFiles(git, query)
 | 
				
			||||||
        val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
 | 
					        val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
 | 
				
			||||||
        files.map { case (path, text) =>
 | 
					        files.map { case (path, text) =>
 | 
				
			||||||
          val (highlightText, lineNumber)  = getHighlightText(text, query)
 | 
					          val (highlightText, lineNumber)  = getHighlightText(text, query)
 | 
				
			||||||
          FileSearchResult(
 | 
					          FileSearchResult(
 | 
				
			||||||
@@ -60,11 +59,12 @@ RepositorySearchService { self: IssuesService =>
 | 
				
			|||||||
    treeWalk.addTree(revCommit.getTree)
 | 
					    treeWalk.addTree(revCommit.getTree)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val keywords = StringUtil.splitWords(query.toLowerCase)
 | 
					    val keywords = StringUtil.splitWords(query.toLowerCase)
 | 
				
			||||||
    val list = new ListBuffer[(String, String)]
 | 
					    val list = new scala.collection.mutable.ListBuffer[(String, String)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    while (treeWalk.next()) {
 | 
					    while (treeWalk.next()) {
 | 
				
			||||||
      if(treeWalk.getFileMode(0) != FileMode.TREE){
 | 
					      val mode = treeWalk.getFileMode(0)
 | 
				
			||||||
        JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
 | 
					      if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){
 | 
				
			||||||
 | 
					        JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes =>
 | 
				
			||||||
          if(FileUtil.isText(bytes)){
 | 
					          if(FileUtil.isText(bytes)){
 | 
				
			||||||
            val text      = StringUtil.convertFromByteArray(bytes)
 | 
					            val text      = StringUtil.convertFromByteArray(bytes)
 | 
				
			||||||
            val lowerText = text.toLowerCase
 | 
					            val lowerText = text.toLowerCase
 | 
				
			||||||
@@ -107,7 +107,7 @@ object RepositorySearchService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  case class SearchResult(
 | 
					  case class SearchResult(
 | 
				
			||||||
    files : List[(String, String)],
 | 
					    files : List[(String, String)],
 | 
				
			||||||
    issues: List[(Issue, Int, String)])
 | 
					    issues: List[(model.Issue, Int, String)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class IssueSearchResult(
 | 
					  case class IssueSearchResult(
 | 
				
			||||||
    issueId: Int,
 | 
					    issueId: Int,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import model._
 | 
					import model.Profile._
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import profile.simple._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import model.{Repository, Account, Collaborator}
 | 
				
			||||||
import util.JGitUtil
 | 
					import util.JGitUtil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait RepositoryService { self: AccountService =>
 | 
					trait RepositoryService { self: AccountService =>
 | 
				
			||||||
@@ -20,7 +20,8 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
 | 
					  def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
 | 
				
			||||||
                       originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
 | 
					                       originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
 | 
				
			||||||
                       parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
 | 
					                       parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
 | 
				
			||||||
 | 
					                      (implicit s: Session): Unit = {
 | 
				
			||||||
    Repositories insert
 | 
					    Repositories insert
 | 
				
			||||||
      Repository(
 | 
					      Repository(
 | 
				
			||||||
        userName             = userName,
 | 
					        userName             = userName,
 | 
				
			||||||
@@ -39,15 +40,89 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
    IssueId insert (userName, repositoryName, 0)
 | 
					    IssueId insert (userName, repositoryName, 0)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteRepository(userName: String, repositoryName: String): Unit = {
 | 
					  def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String)
 | 
				
			||||||
 | 
					                      (implicit s: Session): Unit = {
 | 
				
			||||||
 | 
					    getAccountByUserName(newUserName).foreach { account =>
 | 
				
			||||||
 | 
					      (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
 | 
				
			||||||
 | 
					        Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val webHooks      = WebHooks     .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val milestones    = Milestones   .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val issueId       = IssueId      .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val issues        = Issues       .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val pullRequests  = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val labels        = Labels       .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val issueLabels   = IssueLabels  .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val activities    = Activities   .filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					        val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Repositories.filter { t =>
 | 
				
			||||||
 | 
					          (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
 | 
				
			||||||
 | 
					        }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Repositories.filter { t =>
 | 
				
			||||||
 | 
					          (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
 | 
				
			||||||
 | 
					        }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        PullRequests.filter { t =>
 | 
				
			||||||
 | 
					          t.requestRepositoryName === oldRepositoryName.bind
 | 
				
			||||||
 | 
					        }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        deleteRepository(oldUserName, oldRepositoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        WebHooks      .insertAll(webHooks      .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        Milestones    .insertAll(milestones    .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        IssueId       .insertAll(issueId       .map(_.copy(_1       = newUserName, _2             = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
 | 
				
			||||||
 | 
					        Issues.insertAll(issues.map { x => x.copy(
 | 
				
			||||||
 | 
					          userName       = newUserName,
 | 
				
			||||||
 | 
					          repositoryName = newRepositoryName,
 | 
				
			||||||
 | 
					          milestoneId    = x.milestoneId.map { id =>
 | 
				
			||||||
 | 
					            newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        )} :_*)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        PullRequests  .insertAll(pullRequests  .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        Labels        .insertAll(labels        .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        IssueLabels   .insertAll(issueLabels   .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        Activities    .insertAll(activities    .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        if(account.isGroupAccount){
 | 
				
			||||||
 | 
					          Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update activity messages
 | 
				
			||||||
 | 
					        val updateActivities = Activities.filter { t =>
 | 
				
			||||||
 | 
					          (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
 | 
				
			||||||
 | 
					          (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
 | 
				
			||||||
 | 
					        }.map { t => t.activityId -> t.message }.list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updateActivities.foreach { case (activityId, message) =>
 | 
				
			||||||
 | 
					          Activities.filter(_.activityId === activityId.bind).map(_.message).update(
 | 
				
			||||||
 | 
					            message
 | 
				
			||||||
 | 
					              .replace(s"[repo:${oldUserName}/${oldRepositoryName}]"   ,s"[repo:${newUserName}/${newRepositoryName}]")
 | 
				
			||||||
 | 
					              .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#")
 | 
				
			||||||
 | 
					              .replace(s"[tag:${oldUserName}/${oldRepositoryName}#"    ,s"[tag:${newUserName}/${newRepositoryName}#")
 | 
				
			||||||
 | 
					              .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
 | 
				
			||||||
 | 
					              .replace(s"[issue:${oldUserName}/${oldRepositoryName}#"  ,s"[issue:${newUserName}/${newRepositoryName}#")
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
 | 
				
			||||||
    Activities    .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    Activities    .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    CommitLog     .filter(_.byRepository(userName, repositoryName)).delete
 | 
					 | 
				
			||||||
    Collaborators .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    Collaborators .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    IssueLabels   .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    IssueLabels   .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    Labels        .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    Labels        .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    IssueComments .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    IssueComments .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    Issues        .filter(_.byRepository(userName, repositoryName)).delete
 | 
					 | 
				
			||||||
    PullRequests  .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    PullRequests  .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
 | 
					    Issues        .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    IssueId       .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    IssueId       .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    Milestones    .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    Milestones    .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
    WebHooks      .filter(_.byRepository(userName, repositoryName)).delete
 | 
					    WebHooks      .filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
@@ -60,8 +135,8 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param userName the user name of repository owner
 | 
					   * @param userName the user name of repository owner
 | 
				
			||||||
   * @return the list of repository names
 | 
					   * @return the list of repository names
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getRepositoryNamesOfUser(userName: String): List[String] =
 | 
					  def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] =
 | 
				
			||||||
    Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
 | 
					    Repositories filter(_.userName === userName.bind) map (_.repositoryName) list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the specified repository information.
 | 
					   * Returns the specified repository information.
 | 
				
			||||||
@@ -71,11 +146,11 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param baseUrl the base url of this application
 | 
					   * @param baseUrl the base url of this application
 | 
				
			||||||
   * @return the repository information
 | 
					   * @return the repository information
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
 | 
					  def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
 | 
				
			||||||
    (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
 | 
					    (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
 | 
				
			||||||
      // for getting issue count and pull request count
 | 
					      // for getting issue count and pull request count
 | 
				
			||||||
      val issues = Query(Issues).filter { t =>
 | 
					      val issues = Issues.filter { t =>
 | 
				
			||||||
        t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
 | 
					        t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind)
 | 
				
			||||||
      }.map(_.pullRequest).list
 | 
					      }.map(_.pullRequest).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      new RepositoryInfo(
 | 
					      new RepositoryInfo(
 | 
				
			||||||
@@ -86,22 +161,35 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
        getForkedCount(
 | 
					        getForkedCount(
 | 
				
			||||||
          repository.originUserName.getOrElse(repository.userName),
 | 
					          repository.originUserName.getOrElse(repository.userName),
 | 
				
			||||||
          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
					          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
				
			||||||
        ))
 | 
					        ),
 | 
				
			||||||
 | 
					        getRepositoryManagers(repository.userName))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
 | 
					  def getAllRepositories()(implicit s: Session): List[(String, String)] = {
 | 
				
			||||||
    Query(Repositories).filter { t1 =>
 | 
					    Repositories.sortBy(_.lastActivityDate desc).map{ t =>
 | 
				
			||||||
      (t1.userName is userName.bind) ||
 | 
					      (t.userName, t.repositoryName)
 | 
				
			||||||
        (Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
 | 
					    }.list
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
 | 
				
			||||||
 | 
					                         (implicit s: Session): List[RepositoryInfo] = {
 | 
				
			||||||
 | 
					    Repositories.filter { t1 =>
 | 
				
			||||||
 | 
					      (t1.userName === userName.bind) ||
 | 
				
			||||||
 | 
					        (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
 | 
				
			||||||
    }.sortBy(_.lastActivityDate desc).list.map{ repository =>
 | 
					    }.sortBy(_.lastActivityDate desc).list.map{ repository =>
 | 
				
			||||||
      new RepositoryInfo(
 | 
					      new RepositoryInfo(
 | 
				
			||||||
        JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
 | 
					        if(withoutPhysicalInfo){
 | 
				
			||||||
 | 
					          new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        repository,
 | 
					        repository,
 | 
				
			||||||
        getForkedCount(
 | 
					        getForkedCount(
 | 
				
			||||||
          repository.originUserName.getOrElse(repository.userName),
 | 
					          repository.originUserName.getOrElse(repository.userName),
 | 
				
			||||||
          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
					          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
				
			||||||
        ))
 | 
					        ),
 | 
				
			||||||
 | 
					        getRepositoryManagers(repository.userName))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,45 +200,61 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param loginAccount the logged in account
 | 
					   * @param loginAccount the logged in account
 | 
				
			||||||
   * @param baseUrl the base url of this application
 | 
					   * @param baseUrl the base url of this application
 | 
				
			||||||
   * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
 | 
					   * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
 | 
				
			||||||
 | 
					   * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
 | 
				
			||||||
 | 
					   *                            branches and tags
 | 
				
			||||||
   * @return the repository information which is sorted in descending order of lastActivityDate.
 | 
					   * @return the repository information which is sorted in descending order of lastActivityDate.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
 | 
					  def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
 | 
				
			||||||
 | 
					                             withoutPhysicalInfo: Boolean = false)
 | 
				
			||||||
 | 
					                            (implicit s: Session): List[RepositoryInfo] = {
 | 
				
			||||||
    (loginAccount match {
 | 
					    (loginAccount match {
 | 
				
			||||||
      // for Administrators
 | 
					      // for Administrators
 | 
				
			||||||
      case Some(x) if(x.isAdmin) => Query(Repositories)
 | 
					      case Some(x) if(x.isAdmin) => Repositories
 | 
				
			||||||
      // for Normal Users
 | 
					      // for Normal Users
 | 
				
			||||||
      case Some(x) if(!x.isAdmin) =>
 | 
					      case Some(x) if(!x.isAdmin) =>
 | 
				
			||||||
        Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
 | 
					        Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) ||
 | 
				
			||||||
          (Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
 | 
					          (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      // for Guests
 | 
					      // for Guests
 | 
				
			||||||
      case None => Query(Repositories) filter(_.isPrivate is false.bind)
 | 
					      case None => Repositories filter(_.isPrivate === false.bind)
 | 
				
			||||||
    }).filter { t =>
 | 
					    }).filter { t =>
 | 
				
			||||||
      repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
 | 
					      repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true)
 | 
				
			||||||
    }.sortBy(_.lastActivityDate desc).list.map{ repository =>
 | 
					    }.sortBy(_.lastActivityDate desc).list.map{ repository =>
 | 
				
			||||||
      new RepositoryInfo(
 | 
					      new RepositoryInfo(
 | 
				
			||||||
        JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
 | 
					        if(withoutPhysicalInfo){
 | 
				
			||||||
 | 
					          new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        repository,
 | 
					        repository,
 | 
				
			||||||
        getForkedCount(
 | 
					        getForkedCount(
 | 
				
			||||||
          repository.originUserName.getOrElse(repository.userName),
 | 
					          repository.originUserName.getOrElse(repository.userName),
 | 
				
			||||||
          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
					          repository.originRepositoryName.getOrElse(repository.repositoryName)
 | 
				
			||||||
        ))
 | 
					        ),
 | 
				
			||||||
 | 
					        getRepositoryManagers(repository.userName))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] =
 | 
				
			||||||
 | 
					    if(getAccountByUserName(userName).exists(_.isGroupAccount)){
 | 
				
			||||||
 | 
					      getGroupMembers(userName).collect { case x if(x.isManager) => x.userName }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      Seq(userName)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Updates the last activity date of the repository.
 | 
					   * Updates the last activity date of the repository.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def updateLastActivityDate(userName: String, repositoryName: String): Unit =
 | 
					  def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
 | 
					    Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Save repository options.
 | 
					   * Save repository options.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def saveRepositoryOptions(userName: String, repositoryName: String, 
 | 
					  def saveRepositoryOptions(userName: String, repositoryName: String, 
 | 
				
			||||||
      description: Option[String], defaultBranch: String, isPrivate: Boolean): Unit =
 | 
					      description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
 | 
				
			||||||
    Repositories.filter(_.byRepository(userName, repositoryName))
 | 
					    Repositories.filter(_.byRepository(userName, repositoryName))
 | 
				
			||||||
      .map { r => r.description.? ~ r.defaultBranch ~ r.isPrivate ~ r.updatedDate }
 | 
					      .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
 | 
				
			||||||
      .update (description, defaultBranch, isPrivate, currentDate)
 | 
					      .update (description, defaultBranch, isPrivate, currentDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -160,8 +264,8 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param repositoryName the repository name
 | 
					   * @param repositoryName the repository name
 | 
				
			||||||
   * @param collaboratorName the collaborator name
 | 
					   * @param collaboratorName the collaborator name
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def addCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
 | 
					  def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Collaborators insert(Collaborator(userName, repositoryName, collaboratorName))
 | 
					    Collaborators insert Collaborator(userName, repositoryName, collaboratorName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Remove collaborator from the repository.
 | 
					   * Remove collaborator from the repository.
 | 
				
			||||||
@@ -170,7 +274,7 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param repositoryName the repository name
 | 
					   * @param repositoryName the repository name
 | 
				
			||||||
   * @param collaboratorName the collaborator name
 | 
					   * @param collaboratorName the collaborator name
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String): Unit =
 | 
					  def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
 | 
					    Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -179,7 +283,7 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param userName the user name of the repository owner
 | 
					   * @param userName the user name of the repository owner
 | 
				
			||||||
   * @param repositoryName the repository name
 | 
					   * @param repositoryName the repository name
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def removeCollaborators(userName: String, repositoryName: String): Unit =
 | 
					  def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
 | 
				
			||||||
    Collaborators.filter(_.byRepository(userName, repositoryName)).delete
 | 
					    Collaborators.filter(_.byRepository(userName, repositoryName)).delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -189,10 +293,10 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
   * @param repositoryName the repository name
 | 
					   * @param repositoryName the repository name
 | 
				
			||||||
   * @return the list of collaborators name
 | 
					   * @return the list of collaborators name
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getCollaborators(userName: String, repositoryName: String): List[String] =
 | 
					  def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] =
 | 
				
			||||||
    Query(Collaborators).filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
 | 
					    Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account]): Boolean = {
 | 
					  def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
 | 
				
			||||||
    loginAccount match {
 | 
					    loginAccount match {
 | 
				
			||||||
      case Some(a) if(a.isAdmin) => true
 | 
					      case Some(a) if(a.isAdmin) => true
 | 
				
			||||||
      case Some(a) if(a.userName == owner) => true
 | 
					      case Some(a) if(a.userName == owner) => true
 | 
				
			||||||
@@ -201,37 +305,41 @@ trait RepositoryService { self: AccountService =>
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def getForkedCount(userName: String, repositoryName: String): Int =
 | 
					  private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
 | 
				
			||||||
    Query(Repositories.filter { t =>
 | 
					    Query(Repositories.filter { t =>
 | 
				
			||||||
      (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
 | 
					      (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
 | 
				
			||||||
    }.length).first
 | 
					    }.length).first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getForkedRepositories(userName: String, repositoryName: String): List[String] =
 | 
					  def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
 | 
				
			||||||
    Query(Repositories).filter { t =>
 | 
					    Repositories.filter { t =>
 | 
				
			||||||
      (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
 | 
					      (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    .sortBy(_.userName asc).map(_.userName).list
 | 
					    .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object RepositoryService {
 | 
					object RepositoryService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
 | 
					  case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
 | 
				
			||||||
    issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
 | 
					    issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
 | 
				
			||||||
    branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
 | 
					    branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Creates instance with issue count and pull request count.
 | 
					     * Creates instance with issue count and pull request count.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
 | 
					    def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
 | 
				
			||||||
      this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
 | 
					      this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Creates instance without issue count and pull request count.
 | 
					     * Creates instance without issue count and pull request count.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
 | 
					    def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
 | 
				
			||||||
      this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
 | 
					      this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
 | 
					  case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import model._
 | 
					import model.{Account, Issue, Session}
 | 
				
			||||||
import service.SystemSettingsService.SystemSettings
 | 
					import util.Implicits.request2Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This service is used for a view helper mainly.
 | 
					 * This service is used for a view helper mainly.
 | 
				
			||||||
@@ -9,29 +9,29 @@ import service.SystemSettingsService.SystemSettings
 | 
				
			|||||||
 * It may be called many times in one request, so each method stores
 | 
					 * It may be called many times in one request, so each method stores
 | 
				
			||||||
 * its result into the cache which available during a request.
 | 
					 * its result into the cache which available during a request.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait RequestCache {
 | 
					trait RequestCache extends SystemSettingsService with AccountService with IssuesService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getSystemSettings()(implicit context: app.Context): SystemSettings =
 | 
					  private implicit def context2Session(implicit context: app.Context): Session =
 | 
				
			||||||
    context.cache("system_settings"){
 | 
					    request2Session(context.request)
 | 
				
			||||||
      new SystemSettingsService {}.loadSystemSettings()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
 | 
					  def getIssue(userName: String, repositoryName: String, issueId: String)
 | 
				
			||||||
 | 
					              (implicit context: app.Context): Option[Issue] = {
 | 
				
			||||||
    context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
 | 
					    context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
 | 
				
			||||||
      new IssuesService {}.getIssue(userName, repositoryName, issueId)
 | 
					      super.getIssue(userName, repositoryName, issueId)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getAccountByUserName(userName: String)(implicit context: app.Context): Option[Account] = {
 | 
					  def getAccountByUserName(userName: String)
 | 
				
			||||||
 | 
					                          (implicit context: app.Context): Option[Account] = {
 | 
				
			||||||
    context.cache(s"account.${userName}"){
 | 
					    context.cache(s"account.${userName}"){
 | 
				
			||||||
      new AccountService {}.getAccountByUserName(userName)
 | 
					      super.getAccountByUserName(userName)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getAccountByMailAddress(mailAddress: String)(implicit context: app.Context): Option[Account] = {
 | 
					  def getAccountByMailAddress(mailAddress: String)
 | 
				
			||||||
 | 
					                             (implicit context: app.Context): Option[Account] = {
 | 
				
			||||||
    context.cache(s"account.${mailAddress}"){
 | 
					    context.cache(s"account.${mailAddress}"){
 | 
				
			||||||
      new AccountService {}.getAccountByMailAddress(mailAddress)
 | 
					      super.getAccountByMailAddress(mailAddress)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								src/main/scala/service/SshKeyService.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/main/scala/service/SshKeyService.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import model.Profile._
 | 
				
			||||||
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.SshKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait SshKeyService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
 | 
				
			||||||
 | 
					    SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
 | 
				
			||||||
 | 
					    SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,14 +3,20 @@ package service
 | 
				
			|||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import SystemSettingsService._
 | 
					import SystemSettingsService._
 | 
				
			||||||
 | 
					import javax.servlet.http.HttpServletRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait SystemSettingsService {
 | 
					trait SystemSettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def saveSystemSettings(settings: SystemSettings): Unit = {
 | 
					  def saveSystemSettings(settings: SystemSettings): Unit = {
 | 
				
			||||||
    defining(new java.util.Properties()){ props =>
 | 
					    defining(new java.util.Properties()){ props =>
 | 
				
			||||||
 | 
					      settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
 | 
				
			||||||
      props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
 | 
					      props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
 | 
				
			||||||
      props.setProperty(Gravatar, settings.gravatar.toString)
 | 
					      props.setProperty(Gravatar, settings.gravatar.toString)
 | 
				
			||||||
      props.setProperty(Notification, settings.notification.toString)
 | 
					      props.setProperty(Notification, settings.notification.toString)
 | 
				
			||||||
 | 
					      props.setProperty(Ssh, settings.ssh.toString)
 | 
				
			||||||
 | 
					      settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
 | 
				
			||||||
      if(settings.notification) {
 | 
					      if(settings.notification) {
 | 
				
			||||||
        settings.smtp.foreach { smtp =>
 | 
					        settings.smtp.foreach { smtp =>
 | 
				
			||||||
          props.setProperty(SmtpHost, smtp.host)
 | 
					          props.setProperty(SmtpHost, smtp.host)
 | 
				
			||||||
@@ -31,10 +37,16 @@ trait SystemSettingsService {
 | 
				
			|||||||
          ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
 | 
					          ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
 | 
				
			||||||
          props.setProperty(LdapBaseDN, ldap.baseDN)
 | 
					          props.setProperty(LdapBaseDN, ldap.baseDN)
 | 
				
			||||||
          props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
 | 
					          props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
 | 
				
			||||||
          props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
 | 
					          ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
 | 
				
			||||||
 | 
					          ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
 | 
				
			||||||
 | 
					          ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
 | 
				
			||||||
 | 
					          ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
 | 
				
			||||||
 | 
					          ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      props.store(new java.io.FileOutputStream(GitBucketConf), null)
 | 
					      using(new java.io.FileOutputStream(GitBucketConf)){ out =>
 | 
				
			||||||
 | 
					        props.store(out, null)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,12 +54,17 @@ trait SystemSettingsService {
 | 
				
			|||||||
  def loadSystemSettings(): SystemSettings = {
 | 
					  def loadSystemSettings(): SystemSettings = {
 | 
				
			||||||
    defining(new java.util.Properties()){ props =>
 | 
					    defining(new java.util.Properties()){ props =>
 | 
				
			||||||
      if(GitBucketConf.exists){
 | 
					      if(GitBucketConf.exists){
 | 
				
			||||||
        props.load(new java.io.FileInputStream(GitBucketConf))
 | 
					        using(new java.io.FileInputStream(GitBucketConf)){ in =>
 | 
				
			||||||
 | 
					          props.load(in)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      SystemSettings(
 | 
					      SystemSettings(
 | 
				
			||||||
 | 
					        getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
 | 
				
			||||||
        getValue(props, AllowAccountRegistration, false),
 | 
					        getValue(props, AllowAccountRegistration, false),
 | 
				
			||||||
        getValue(props, Gravatar, true),
 | 
					        getValue(props, Gravatar, true),
 | 
				
			||||||
        getValue(props, Notification, false),
 | 
					        getValue(props, Notification, false),
 | 
				
			||||||
 | 
					        getValue(props, Ssh, false),
 | 
				
			||||||
 | 
					        getOptionValue(props, SshPort, Some(DefaultSshPort)),
 | 
				
			||||||
        if(getValue(props, Notification, false)){
 | 
					        if(getValue(props, Notification, false)){
 | 
				
			||||||
          Some(Smtp(
 | 
					          Some(Smtp(
 | 
				
			||||||
            getValue(props, SmtpHost, ""),
 | 
					            getValue(props, SmtpHost, ""),
 | 
				
			||||||
@@ -69,7 +86,11 @@ trait SystemSettingsService {
 | 
				
			|||||||
            getOptionValue(props, LdapBindPassword, None),
 | 
					            getOptionValue(props, LdapBindPassword, None),
 | 
				
			||||||
            getValue(props, LdapBaseDN, ""),
 | 
					            getValue(props, LdapBaseDN, ""),
 | 
				
			||||||
            getValue(props, LdapUserNameAttribute, ""),
 | 
					            getValue(props, LdapUserNameAttribute, ""),
 | 
				
			||||||
            getValue(props, LdapMailAddressAttribute, "")))
 | 
					            getOptionValue(props, LdapAdditionalFilterCondition, None),
 | 
				
			||||||
 | 
					            getOptionValue(props, LdapFullNameAttribute, None),
 | 
				
			||||||
 | 
					            getOptionValue(props, LdapMailAddressAttribute, None),
 | 
				
			||||||
 | 
					            getOptionValue[Boolean](props, LdapTls, None),
 | 
				
			||||||
 | 
					            getOptionValue(props, LdapKeystore, None)))
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          None
 | 
					          None
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -83,12 +104,21 @@ object SystemSettingsService {
 | 
				
			|||||||
  import scala.reflect.ClassTag
 | 
					  import scala.reflect.ClassTag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class SystemSettings(
 | 
					  case class SystemSettings(
 | 
				
			||||||
 | 
					    baseUrl: Option[String],
 | 
				
			||||||
    allowAccountRegistration: Boolean,
 | 
					    allowAccountRegistration: Boolean,
 | 
				
			||||||
    gravatar: Boolean,
 | 
					    gravatar: Boolean,
 | 
				
			||||||
    notification: Boolean,
 | 
					    notification: Boolean,
 | 
				
			||||||
 | 
					    ssh: Boolean,
 | 
				
			||||||
 | 
					    sshPort: Option[Int],
 | 
				
			||||||
    smtp: Option[Smtp],
 | 
					    smtp: Option[Smtp],
 | 
				
			||||||
    ldapAuthentication: Boolean,
 | 
					    ldapAuthentication: Boolean,
 | 
				
			||||||
    ldap: Option[Ldap])
 | 
					    ldap: Option[Ldap]){
 | 
				
			||||||
 | 
					    def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
 | 
				
			||||||
 | 
					      defining(request.getRequestURL.toString){ url =>
 | 
				
			||||||
 | 
					        url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }.stripSuffix("/")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class Ldap(
 | 
					  case class Ldap(
 | 
				
			||||||
    host: String,
 | 
					    host: String,
 | 
				
			||||||
@@ -97,7 +127,11 @@ object SystemSettingsService {
 | 
				
			|||||||
    bindPassword: Option[String],
 | 
					    bindPassword: Option[String],
 | 
				
			||||||
    baseDN: String,
 | 
					    baseDN: String,
 | 
				
			||||||
    userNameAttribute: String,
 | 
					    userNameAttribute: String,
 | 
				
			||||||
    mailAttribute: String)
 | 
					    additionalFilterCondition: Option[String],
 | 
				
			||||||
 | 
					    fullNameAttribute: Option[String],
 | 
				
			||||||
 | 
					    mailAttribute: Option[String],
 | 
				
			||||||
 | 
					    tls: Option[Boolean],
 | 
				
			||||||
 | 
					    keystore: Option[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class Smtp(
 | 
					  case class Smtp(
 | 
				
			||||||
    host: String,
 | 
					    host: String,
 | 
				
			||||||
@@ -108,12 +142,16 @@ object SystemSettingsService {
 | 
				
			|||||||
    fromAddress: Option[String],
 | 
					    fromAddress: Option[String],
 | 
				
			||||||
    fromName: Option[String])
 | 
					    fromName: Option[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  val DefaultSshPort = 29418
 | 
				
			||||||
  val DefaultSmtpPort = 25
 | 
					  val DefaultSmtpPort = 25
 | 
				
			||||||
  val DefaultLdapPort = 389
 | 
					  val DefaultLdapPort = 389
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val BaseURL = "base_url"
 | 
				
			||||||
  private val AllowAccountRegistration = "allow_account_registration"
 | 
					  private val AllowAccountRegistration = "allow_account_registration"
 | 
				
			||||||
  private val Gravatar = "gravatar"
 | 
					  private val Gravatar = "gravatar"
 | 
				
			||||||
  private val Notification = "notification"
 | 
					  private val Notification = "notification"
 | 
				
			||||||
 | 
					  private val Ssh = "ssh"
 | 
				
			||||||
 | 
					  private val SshPort = "ssh.port"
 | 
				
			||||||
  private val SmtpHost = "smtp.host"
 | 
					  private val SmtpHost = "smtp.host"
 | 
				
			||||||
  private val SmtpPort = "smtp.port"
 | 
					  private val SmtpPort = "smtp.port"
 | 
				
			||||||
  private val SmtpUser = "smtp.user"
 | 
					  private val SmtpUser = "smtp.user"
 | 
				
			||||||
@@ -128,7 +166,11 @@ object SystemSettingsService {
 | 
				
			|||||||
  private val LdapBindPassword = "ldap.bind_password"
 | 
					  private val LdapBindPassword = "ldap.bind_password"
 | 
				
			||||||
  private val LdapBaseDN = "ldap.baseDN"
 | 
					  private val LdapBaseDN = "ldap.baseDN"
 | 
				
			||||||
  private val LdapUserNameAttribute = "ldap.username_attribute"
 | 
					  private val LdapUserNameAttribute = "ldap.username_attribute"
 | 
				
			||||||
 | 
					  private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
 | 
				
			||||||
 | 
					  private val LdapFullNameAttribute = "ldap.fullname_attribute"
 | 
				
			||||||
  private val LdapMailAddressAttribute = "ldap.mail_attribute"
 | 
					  private val LdapMailAddressAttribute = "ldap.mail_attribute"
 | 
				
			||||||
 | 
					  private val LdapTls = "ldap.tls"
 | 
				
			||||||
 | 
					  private val LdapKeystore = "ldap.keystore"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
 | 
					  private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
 | 
				
			||||||
    defining(props.getProperty(key)){ value =>
 | 
					    defining(props.getProperty(key)){ value =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,8 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.slick.driver.H2Driver.simple._
 | 
					import model.Profile._
 | 
				
			||||||
import Database.threadLocalSession
 | 
					import profile.simple._
 | 
				
			||||||
 | 
					import model.{WebHook, Account}
 | 
				
			||||||
import model._
 | 
					 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import service.RepositoryService.RepositoryInfo
 | 
					import service.RepositoryService.RepositoryInfo
 | 
				
			||||||
import util.JGitUtil
 | 
					import util.JGitUtil
 | 
				
			||||||
@@ -12,7 +11,6 @@ import util.JGitUtil.CommitInfo
 | 
				
			|||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import org.apache.http.message.BasicNameValuePair
 | 
					import org.apache.http.message.BasicNameValuePair
 | 
				
			||||||
import org.apache.http.client.entity.UrlEncodedFormEntity
 | 
					import org.apache.http.client.entity.UrlEncodedFormEntity
 | 
				
			||||||
import org.apache.http.protocol.HTTP
 | 
					 | 
				
			||||||
import org.apache.http.NameValuePair
 | 
					import org.apache.http.NameValuePair
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait WebHookService {
 | 
					trait WebHookService {
 | 
				
			||||||
@@ -20,14 +18,14 @@ trait WebHookService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[WebHookService])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[WebHookService])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getWebHookURLs(owner: String, repository: String): List[WebHook] =
 | 
					  def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
 | 
				
			||||||
    Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
 | 
					    WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def addWebHookURL(owner: String, repository: String, url :String): Unit =
 | 
					  def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
 | 
				
			||||||
    WebHooks.insert(WebHook(owner, repository, url))
 | 
					    WebHooks insert WebHook(owner, repository, url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
 | 
					  def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
 | 
				
			||||||
    Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
 | 
					    WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
 | 
					  def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
 | 
				
			||||||
    import org.json4s._
 | 
					    import org.json4s._
 | 
				
			||||||
@@ -46,7 +44,7 @@ trait WebHookService {
 | 
				
			|||||||
      val httpClient = HttpClientBuilder.create.build
 | 
					      val httpClient = HttpClientBuilder.create.build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      webHookURLs.foreach { webHookUrl =>
 | 
					      webHookURLs.foreach { webHookUrl =>
 | 
				
			||||||
        val f = future {
 | 
					        val f = Future {
 | 
				
			||||||
          logger.debug(s"start web hook invocation for ${webHookUrl}")
 | 
					          logger.debug(s"start web hook invocation for ${webHookUrl}")
 | 
				
			||||||
          val httpPost = new HttpPost(webHookUrl.url)
 | 
					          val httpPost = new HttpPost(webHookUrl.url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,37 +72,39 @@ trait WebHookService {
 | 
				
			|||||||
object WebHookService {
 | 
					object WebHookService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class WebHookPayload(
 | 
					  case class WebHookPayload(
 | 
				
			||||||
 | 
					    pusher: WebHookUser,
 | 
				
			||||||
    ref: String,
 | 
					    ref: String,
 | 
				
			||||||
    commits: List[WebHookCommit],
 | 
					    commits: List[WebHookCommit],
 | 
				
			||||||
    repository: WebHookRepository)
 | 
					    repository: WebHookRepository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  object WebHookPayload {
 | 
					  object WebHookPayload {
 | 
				
			||||||
    def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
 | 
					    def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
 | 
				
			||||||
              commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
 | 
					              commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
 | 
				
			||||||
      WebHookPayload(
 | 
					      WebHookPayload(
 | 
				
			||||||
 | 
					        WebHookUser(pusher.fullName, pusher.mailAddress),
 | 
				
			||||||
        refName,
 | 
					        refName,
 | 
				
			||||||
        commits.map { commit =>
 | 
					        commits.map { commit =>
 | 
				
			||||||
          val diffs = JGitUtil.getDiffs(git, commit.id, false)
 | 
					          val diffs = JGitUtil.getDiffs(git, commit.id, false)
 | 
				
			||||||
          val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
 | 
					          val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          WebHookCommit(
 | 
					          WebHookCommit(
 | 
				
			||||||
            id        = commit.id,
 | 
					            id        = commit.id,
 | 
				
			||||||
            message   = commit.fullMessage,
 | 
					            message   = commit.fullMessage,
 | 
				
			||||||
            timestamp = commit.time.toString,
 | 
					            timestamp = commit.commitTime.toString,
 | 
				
			||||||
            url       = commitUrl,
 | 
					            url       = commitUrl,
 | 
				
			||||||
            added     = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD)    => x.newPath },
 | 
					            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 },
 | 
					            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 &&
 | 
					            modified  = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
 | 
				
			||||||
              x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
 | 
					              x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
 | 
				
			||||||
            author    = WebHookUser(
 | 
					            author    = WebHookUser(
 | 
				
			||||||
              name  = commit.committer,
 | 
					              name  = commit.committerName,
 | 
				
			||||||
              email = commit.mailAddress
 | 
					              email = commit.committerEmailAddress
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        }.toList,
 | 
					        },
 | 
				
			||||||
        WebHookRepository(
 | 
					        WebHookRepository(
 | 
				
			||||||
          name        = repositoryInfo.name,
 | 
					          name        = repositoryInfo.name,
 | 
				
			||||||
          url         = repositoryInfo.url,
 | 
					          url         = repositoryInfo.httpUrl,
 | 
				
			||||||
          description = repositoryInfo.repository.description.getOrElse(""),
 | 
					          description = repositoryInfo.repository.description.getOrElse(""),
 | 
				
			||||||
          watchers    = 0,
 | 
					          watchers    = 0,
 | 
				
			||||||
          forks       = repositoryInfo.forkedCount,
 | 
					          forks       = repositoryInfo.forkedCount,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,18 @@
 | 
				
			|||||||
package service
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.File
 | 
					 | 
				
			||||||
import java.util.Date
 | 
					import java.util.Date
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import org.apache.commons.io.FileUtils
 | 
					import util._
 | 
				
			||||||
import util.{StringUtil, Directory, JGitUtil, LockUtil}
 | 
					import _root_.util.ControlUtil._
 | 
				
			||||||
import util.ControlUtil._
 | 
					 | 
				
			||||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser
 | 
					import org.eclipse.jgit.treewalk.CanonicalTreeParser
 | 
				
			||||||
import org.eclipse.jgit.diff.DiffFormatter
 | 
					import org.eclipse.jgit.lib._
 | 
				
			||||||
import org.eclipse.jgit.api.errors.PatchApplyException
 | 
					import org.eclipse.jgit.dircache.DirCache
 | 
				
			||||||
 | 
					import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
 | 
				
			||||||
 | 
					import java.io.ByteArrayInputStream
 | 
				
			||||||
 | 
					import org.eclipse.jgit.patch._
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.errors.PatchFormatException
 | 
				
			||||||
 | 
					import scala.collection.JavaConverters._
 | 
				
			||||||
 | 
					import service.RepositoryService.RepositoryInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object WikiService {
 | 
					object WikiService {
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -33,6 +37,10 @@ object WikiService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
 | 
					  case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
 | 
				
			||||||
 | 
					    repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait WikiService {
 | 
					trait WikiService {
 | 
				
			||||||
@@ -42,13 +50,8 @@ trait WikiService {
 | 
				
			|||||||
    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
					    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
				
			||||||
      defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
 | 
					      defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
 | 
				
			||||||
        if(!dir.exists){
 | 
					        if(!dir.exists){
 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
          JGitUtil.initRepository(dir)
 | 
					          JGitUtil.initRepository(dir)
 | 
				
			||||||
          saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
 | 
					          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))
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -58,11 +61,12 @@ trait WikiService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
 | 
					  def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
 | 
				
			||||||
    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
					    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
      optionIf(!JGitUtil.isEmpty(git)){
 | 
					      if(!JGitUtil.isEmpty(git)){
 | 
				
			||||||
        JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
 | 
					        JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
 | 
				
			||||||
          WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
 | 
					          WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes),
 | 
				
			||||||
        }
 | 
					                       file.author, file.time, file.commitId)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      } else None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,7 +75,7 @@ trait WikiService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
 | 
					  def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
 | 
				
			||||||
    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
					    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
      optionIf(!JGitUtil.isEmpty(git)){
 | 
					      if(!JGitUtil.isEmpty(git)){
 | 
				
			||||||
        val index = path.lastIndexOf('/')
 | 
					        val index = path.lastIndexOf('/')
 | 
				
			||||||
        val parentPath = if(index < 0) "."  else path.substring(0, index)
 | 
					        val parentPath = if(index < 0) "."  else path.substring(0, index)
 | 
				
			||||||
        val fileName   = if(index < 0) path else path.substring(index + 1)
 | 
					        val fileName   = if(index < 0) path else path.substring(index + 1)
 | 
				
			||||||
@@ -79,7 +83,7 @@ trait WikiService {
 | 
				
			|||||||
        JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
 | 
					        JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
 | 
				
			||||||
          git.getRepository.open(file.id).getBytes
 | 
					          git.getRepository.open(file.id).getBytes
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      } else None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -89,7 +93,7 @@ trait WikiService {
 | 
				
			|||||||
    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
					    using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
      JGitUtil.getFileList(git, "master", ".")
 | 
					      JGitUtil.getFileList(git, "master", ".")
 | 
				
			||||||
        .filter(_.name.endsWith(".md"))
 | 
					        .filter(_.name.endsWith(".md"))
 | 
				
			||||||
        .map(_.name.replaceFirst("\\.md$", ""))
 | 
					        .map(_.name.stripSuffix(".md"))
 | 
				
			||||||
        .sortBy(x => x)
 | 
					        .sortBy(x => x)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -99,12 +103,13 @@ trait WikiService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def revertWikiPage(owner: String, repository: String, from: String, to: String,
 | 
					  def revertWikiPage(owner: String, repository: String, from: String, to: String,
 | 
				
			||||||
                     committer: model.Account, pageName: Option[String]): Boolean = {
 | 
					                     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 =>
 | 
					    case class RevertInfo(operation: String, filePath: String, source: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
				
			||||||
 | 
					        using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          val reader = git.getRepository.newObjectReader
 | 
					          val reader = git.getRepository.newObjectReader
 | 
				
			||||||
          val oldTreeIter = new CanonicalTreeParser
 | 
					          val oldTreeIter = new CanonicalTreeParser
 | 
				
			||||||
          oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
 | 
					          oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
 | 
				
			||||||
@@ -112,7 +117,6 @@ trait WikiService {
 | 
				
			|||||||
          val newTreeIter = new CanonicalTreeParser
 | 
					          val newTreeIter = new CanonicalTreeParser
 | 
				
			||||||
          newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
 | 
					          newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          import scala.collection.JavaConverters._
 | 
					 | 
				
			||||||
          val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
 | 
					          val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
 | 
				
			||||||
            pageName match {
 | 
					            pageName match {
 | 
				
			||||||
              case Some(x) => diff.getNewPath == x + ".md"
 | 
					              case Some(x) => diff.getNewPath == x + ".md"
 | 
				
			||||||
@@ -127,57 +131,109 @@ trait WikiService {
 | 
				
			|||||||
            new String(out.toByteArray, "UTF-8")
 | 
					            new String(out.toByteArray, "UTF-8")
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          try {
 | 
					          val p = new Patch()
 | 
				
			||||||
            git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call
 | 
					          p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8")))
 | 
				
			||||||
            git.add.addFilepattern(".").call
 | 
					          if(!p.getErrors.isEmpty){
 | 
				
			||||||
            git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match {
 | 
					            throw new PatchFormatException(p.getErrors())
 | 
				
			||||||
              case Some(x) => s"Revert ${from} ... ${to} on ${x}"
 | 
					          }
 | 
				
			||||||
              case None    => s"Revert ${from} ... ${to}"
 | 
					          val revertInfo = (p.getFiles.asScala.map { fh =>
 | 
				
			||||||
            }).call
 | 
					            fh.getChangeType match {
 | 
				
			||||||
            git.push.call
 | 
					              case DiffEntry.ChangeType.MODIFY => {
 | 
				
			||||||
            true
 | 
					                val source = getWikiPage(owner, repository, fh.getNewPath.stripSuffix(".md")).map(_.content).getOrElse("")
 | 
				
			||||||
          } catch {
 | 
					                val applied = PatchUtil.apply(source, patch, fh)
 | 
				
			||||||
            case ex: PatchApplyException => false
 | 
					                if(applied != null){
 | 
				
			||||||
 | 
					                  Seq(RevertInfo("ADD", fh.getNewPath, applied))
 | 
				
			||||||
 | 
					                } else Nil
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              case DiffEntry.ChangeType.ADD => {
 | 
				
			||||||
 | 
					                val applied = PatchUtil.apply("", patch, fh)
 | 
				
			||||||
 | 
					                if(applied != null){
 | 
				
			||||||
 | 
					                  Seq(RevertInfo("ADD", fh.getNewPath, applied))
 | 
				
			||||||
 | 
					                } else Nil
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              case DiffEntry.ChangeType.DELETE => {
 | 
				
			||||||
 | 
					                Seq(RevertInfo("DELETE", fh.getNewPath, ""))
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              case DiffEntry.ChangeType.RENAME => {
 | 
				
			||||||
 | 
					                val applied = PatchUtil.apply("", patch, fh)
 | 
				
			||||||
 | 
					                if(applied != null){
 | 
				
			||||||
 | 
					                  Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied))
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  Seq(RevertInfo("DELETE", fh.getOldPath, ""))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					              case _ => Nil
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					          }).flatten
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if(revertInfo.nonEmpty){
 | 
				
			||||||
 | 
					            val builder  = DirCache.newInCore.builder()
 | 
				
			||||||
 | 
					            val inserter = git.getRepository.newObjectInserter()
 | 
				
			||||||
 | 
					            val headId   = git.getRepository.resolve(Constants.HEAD + "^{commit}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            JGitUtil.processTree(git, headId){ (path, tree) =>
 | 
				
			||||||
 | 
					              if(revertInfo.find(x => x.filePath == path).isEmpty){
 | 
				
			||||||
 | 
					                builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            revertInfo.filter(_.operation == "ADD").foreach { x =>
 | 
				
			||||||
 | 
					              builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8"))))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            builder.finish()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
 | 
				
			||||||
 | 
					              Constants.HEAD, committer.fullName, committer.mailAddress,
 | 
				
			||||||
 | 
					              pageName match {
 | 
				
			||||||
 | 
					                case Some(x) => s"Revert ${from} ... ${to} on ${x}"
 | 
				
			||||||
 | 
					                case None    => s"Revert ${from} ... ${to}"
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      case e: Exception => {
 | 
				
			||||||
 | 
					        e.printStackTrace()
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Save the wiki page.
 | 
					   * Save the wiki page.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
 | 
					  def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
 | 
				
			||||||
      content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
 | 
					      content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
					    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
				
			||||||
      defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
 | 
					      using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
        // clone working copy
 | 
					        val builder  = DirCache.newInCore.builder()
 | 
				
			||||||
        cloneOrPullWorkingCopy(workDir, owner, repository)
 | 
					        val inserter = git.getRepository.newObjectInserter()
 | 
				
			||||||
 | 
					        val headId   = git.getRepository.resolve(Constants.HEAD + "^{commit}")
 | 
				
			||||||
 | 
					        var created  = true
 | 
				
			||||||
 | 
					        var updated  = false
 | 
				
			||||||
 | 
					        var removed  = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // write as file
 | 
					        if(headId != null){
 | 
				
			||||||
        using(Git.open(workDir)){ git =>
 | 
					          JGitUtil.processTree(git, headId){ (path, tree) =>
 | 
				
			||||||
          defining(new File(workDir, newPageName + ".md")){ file =>
 | 
					            if(path == currentPageName + ".md" && currentPageName != newPageName){
 | 
				
			||||||
            // new page
 | 
					              removed = true
 | 
				
			||||||
            val created = !file.exists
 | 
					            } else if(path != newPageName + ".md"){
 | 
				
			||||||
 | 
					              builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
 | 
				
			||||||
            // created or updated
 | 
					            } else {
 | 
				
			||||||
            val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
 | 
					              created = false
 | 
				
			||||||
              FileUtils.writeStringToFile(file, content, "UTF-8")
 | 
					              updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false)
 | 
				
			||||||
              git.add.addFilepattern(file.getName).call
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // delete file
 | 
					        if(created || updated || removed){
 | 
				
			||||||
            val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
 | 
					          builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
 | 
				
			||||||
              git.rm.addFilepattern(currentPageName + ".md").call
 | 
					          builder.finish()
 | 
				
			||||||
            }
 | 
					          val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
 | 
				
			||||||
 | 
					            Constants.HEAD, committer.fullName, committer.mailAddress,
 | 
				
			||||||
            // commit and push
 | 
					            if(message.trim.length == 0) {
 | 
				
			||||||
            optionIf(added || deleted){
 | 
					              if(removed){
 | 
				
			||||||
              defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
 | 
					 | 
				
			||||||
                .setMessage(if(message.trim.length == 0){
 | 
					 | 
				
			||||||
                    if(deleted){
 | 
					 | 
				
			||||||
                s"Rename ${currentPageName} to ${newPageName}"
 | 
					                s"Rename ${currentPageName} to ${newPageName}"
 | 
				
			||||||
              } else if(created){
 | 
					              } else if(created){
 | 
				
			||||||
                s"Created ${newPageName}"
 | 
					                s"Created ${newPageName}"
 | 
				
			||||||
@@ -186,13 +242,10 @@ trait WikiService {
 | 
				
			|||||||
              }
 | 
					              }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              message
 | 
					              message
 | 
				
			||||||
                  }).call){ commit =>
 | 
					            })
 | 
				
			||||||
                git.push.call
 | 
					
 | 
				
			||||||
                Some(commit.getName)
 | 
					          Some(newHeadId.getName)
 | 
				
			||||||
              }
 | 
					        } else None
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -203,34 +256,25 @@ trait WikiService {
 | 
				
			|||||||
  def deleteWikiPage(owner: String, repository: String, pageName: String,
 | 
					  def deleteWikiPage(owner: String, repository: String, pageName: String,
 | 
				
			||||||
                     committer: String, mailAddress: String, message: String): Unit = {
 | 
					                     committer: String, mailAddress: String, message: String): Unit = {
 | 
				
			||||||
    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
					    LockUtil.lock(s"${owner}/${repository}/wiki"){
 | 
				
			||||||
      defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
 | 
					      using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
        // clone working copy
 | 
					        val builder  = DirCache.newInCore.builder()
 | 
				
			||||||
        cloneOrPullWorkingCopy(workDir, owner, repository)
 | 
					        val inserter = git.getRepository.newObjectInserter()
 | 
				
			||||||
 | 
					        val headId   = git.getRepository.resolve(Constants.HEAD + "^{commit}")
 | 
				
			||||||
 | 
					        var removed  = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // delete file
 | 
					        JGitUtil.processTree(git, headId){ (path, tree) =>
 | 
				
			||||||
        new File(workDir, pageName + ".md").delete
 | 
					          if(path != pageName + ".md"){
 | 
				
			||||||
 | 
					            builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
 | 
				
			||||||
        using(Git.open(workDir)){ git =>
 | 
					          } else {
 | 
				
			||||||
          git.rm.addFilepattern(pageName + ".md").call
 | 
					            removed = true
 | 
				
			||||||
 | 
					 | 
				
			||||||
          // commit and push
 | 
					 | 
				
			||||||
          git.commit.setCommitter(committer, mailAddress).setMessage(message).call
 | 
					 | 
				
			||||||
          git.push.call
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if(removed){
 | 
				
			||||||
 | 
					          builder.finish()
 | 
				
			||||||
 | 
					          JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
 | 
				
			||||||
 | 
					            Constants.HEAD, committer, mailAddress, message)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
        .getRepository
 | 
					 | 
				
			||||||
        .close
 | 
					 | 
				
			||||||
    } else using(Git.open(workDir)){ git =>
 | 
					 | 
				
			||||||
      git.pull.call
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory
 | 
				
			|||||||
import util.Directory._
 | 
					import util.Directory._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
 | 
					import util.Directory
 | 
				
			||||||
 | 
					import plugin.PluginUpdateJob
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object AutoUpdate {
 | 
					object AutoUpdate {
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -50,6 +52,61 @@ object AutoUpdate {
 | 
				
			|||||||
   * The history of versions. A head of this sequence is the current BitBucket version.
 | 
					   * The history of versions. A head of this sequence is the current BitBucket version.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  val versions = Seq(
 | 
					  val versions = Seq(
 | 
				
			||||||
 | 
					    new Version(2, 3) {
 | 
				
			||||||
 | 
					      override def update(conn: Connection): Unit = {
 | 
				
			||||||
 | 
					        super.update(conn)
 | 
				
			||||||
 | 
					        using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs =>
 | 
				
			||||||
 | 
					          while(rs.next) {
 | 
				
			||||||
 | 
					            val info = rs.getString("ADDITIONAL_INFO")
 | 
				
			||||||
 | 
					            val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
 | 
				
			||||||
 | 
					            if (info != newInfo) {
 | 
				
			||||||
 | 
					              val id = rs.getString("ACTIVITY_ID")
 | 
				
			||||||
 | 
					              using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql =>
 | 
				
			||||||
 | 
					                sql.setString(1, newInfo)
 | 
				
			||||||
 | 
					                sql.setLong(2, id.toLong)
 | 
				
			||||||
 | 
					                sql.executeUpdate
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(Directory.getPluginCacheDir())
 | 
				
			||||||
 | 
					        FileUtils.deleteDirectory(new File(Directory.PluginHome))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    new Version(2, 2),
 | 
				
			||||||
 | 
					    new Version(2, 1),
 | 
				
			||||||
 | 
					    new Version(2, 0){
 | 
				
			||||||
 | 
					      override def update(conn: Connection): Unit = {
 | 
				
			||||||
 | 
					        import eu.medsea.mimeutil.{MimeUtil2, MimeType}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val mimeUtil = new MimeUtil2()
 | 
				
			||||||
 | 
					        mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super.update(conn)
 | 
				
			||||||
 | 
					        using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
 | 
				
			||||||
 | 
					          while(rs.next){
 | 
				
			||||||
 | 
					            defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
 | 
				
			||||||
 | 
					              if(dir.exists && dir.isDirectory){
 | 
				
			||||||
 | 
					                dir.listFiles.foreach { file =>
 | 
				
			||||||
 | 
					                  if(file.getName.indexOf('.') < 0){
 | 
				
			||||||
 | 
					                    val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
 | 
				
			||||||
 | 
					                    if(mimeType.startsWith("image/")){
 | 
				
			||||||
 | 
					                      file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Version(1, 13),
 | 
				
			||||||
 | 
					    Version(1, 12),
 | 
				
			||||||
 | 
					    Version(1, 11),
 | 
				
			||||||
 | 
					    Version(1, 10),
 | 
				
			||||||
 | 
					    Version(1, 9),
 | 
				
			||||||
 | 
					    Version(1, 8),
 | 
				
			||||||
    Version(1, 7),
 | 
					    Version(1, 7),
 | 
				
			||||||
    Version(1, 6),
 | 
					    Version(1, 6),
 | 
				
			||||||
    Version(1, 5),
 | 
					    Version(1, 5),
 | 
				
			||||||
@@ -86,14 +143,14 @@ object AutoUpdate {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * The version file (GITBUCKET_HOME/version).
 | 
					   * The version file (GITBUCKET_HOME/version).
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  val versionFile = new File(GitBucketHome, "version")
 | 
					  lazy val versionFile = new File(GitBucketHome, "version")
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the current version from the version file.
 | 
					   * Returns the current version from the version file.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getCurrentVersion(): Version = {
 | 
					  def getCurrentVersion(): Version = {
 | 
				
			||||||
    if(versionFile.exists){
 | 
					    if(versionFile.exists){
 | 
				
			||||||
      FileUtils.readFileToString(versionFile, "UTF-8").split("\\.") match {
 | 
					      FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
 | 
				
			||||||
        case Array(majorVersion, minorVersion) => {
 | 
					        case Array(majorVersion, minorVersion) => {
 | 
				
			||||||
          versions.find { v => 
 | 
					          versions.find { v => 
 | 
				
			||||||
            v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
 | 
					            v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
 | 
				
			||||||
@@ -101,10 +158,7 @@ object AutoUpdate {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        case _ => Version(0, 0)
 | 
					        case _ => Version(0, 0)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else Version(0, 0)
 | 
				
			||||||
      Version(0, 0)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -113,15 +167,27 @@ object AutoUpdate {
 | 
				
			|||||||
 * Update database schema automatically in the context initializing.
 | 
					 * Update database schema automatically in the context initializing.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class AutoUpdateListener extends ServletContextListener {
 | 
					class AutoUpdateListener extends ServletContextListener {
 | 
				
			||||||
 | 
					  import org.quartz.impl.StdSchedulerFactory
 | 
				
			||||||
 | 
					  import org.quartz.JobBuilder._
 | 
				
			||||||
 | 
					  import org.quartz.TriggerBuilder._
 | 
				
			||||||
 | 
					  import org.quartz.SimpleScheduleBuilder._
 | 
				
			||||||
  import AutoUpdate._
 | 
					  import AutoUpdate._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
 | 
				
			||||||
 | 
					  private val scheduler = StdSchedulerFactory.getDefaultScheduler
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  override def contextInitialized(event: ServletContextEvent): Unit = {
 | 
					  override def contextInitialized(event: ServletContextEvent): Unit = {
 | 
				
			||||||
 | 
					    val datadir = event.getServletContext.getInitParameter("gitbucket.home")
 | 
				
			||||||
 | 
					    if(datadir != null){
 | 
				
			||||||
 | 
					      System.setProperty("gitbucket.home", datadir)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    org.h2.Driver.load()
 | 
					    org.h2.Driver.load()
 | 
				
			||||||
    event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.debug("Start schema update")
 | 
					    val context = event.getServletContext
 | 
				
			||||||
 | 
					    context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    defining(getConnection(event.getServletContext)){ conn =>
 | 
					    defining(getConnection(event.getServletContext)){ conn =>
 | 
				
			||||||
 | 
					      logger.debug("Start schema update")
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        defining(getCurrentVersion()){ currentVersion =>
 | 
					        defining(getCurrentVersion()){ currentVersion =>
 | 
				
			||||||
          if(currentVersion == headVersion){
 | 
					          if(currentVersion == headVersion){
 | 
				
			||||||
@@ -131,7 +197,6 @@ class AutoUpdateListener extends ServletContextListener {
 | 
				
			|||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
 | 
					            versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
 | 
				
			||||||
            FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
 | 
					            FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
 | 
				
			||||||
            conn.commit()
 | 
					 | 
				
			||||||
            logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
 | 
					            logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -142,12 +207,31 @@ class AutoUpdateListener extends ServletContextListener {
 | 
				
			|||||||
          conn.rollback()
 | 
					          conn.rollback()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
      logger.debug("End schema update")
 | 
					      logger.debug("End schema update")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getDatabase(context).withSession { implicit session =>
 | 
				
			||||||
 | 
					      logger.debug("Starting plugin system...")
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        plugin.PluginSystem.init()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        scheduler.start()
 | 
				
			||||||
 | 
					        PluginUpdateJob.schedule(scheduler)
 | 
				
			||||||
 | 
					        logger.debug("PluginUpdateJob is started.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        logger.debug("Plugin system is initialized.")
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        case ex: Throwable => {
 | 
				
			||||||
 | 
					          logger.error("Failed to initialize plugin system", ex)
 | 
				
			||||||
 | 
					          ex.printStackTrace()
 | 
				
			||||||
 | 
					          throw ex
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def contextDestroyed(sce: ServletContextEvent): Unit = {
 | 
					  def contextDestroyed(sce: ServletContextEvent): Unit = {
 | 
				
			||||||
    // Nothing to do.
 | 
					    scheduler.shutdown()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def getConnection(servletContext: ServletContext): Connection =
 | 
					  private def getConnection(servletContext: ServletContext): Connection =
 | 
				
			||||||
@@ -156,4 +240,10 @@ class AutoUpdateListener extends ServletContextListener {
 | 
				
			|||||||
      servletContext.getInitParameter("db.user"),
 | 
					      servletContext.getInitParameter("db.user"),
 | 
				
			||||||
      servletContext.getInitParameter("db.password"))
 | 
					      servletContext.getInitParameter("db.password"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def getDatabase(servletContext: ServletContext): scala.slick.jdbc.JdbcBackend.Database =
 | 
				
			||||||
 | 
					    slick.jdbc.JdbcBackend.Database.forURL(
 | 
				
			||||||
 | 
					      servletContext.getInitParameter("db.url"),
 | 
				
			||||||
 | 
					      servletContext.getInitParameter("db.user"),
 | 
				
			||||||
 | 
					      servletContext.getInitParameter("db.password"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ package servlet
 | 
				
			|||||||
import javax.servlet._
 | 
					import javax.servlet._
 | 
				
			||||||
import javax.servlet.http._
 | 
					import javax.servlet.http._
 | 
				
			||||||
import service.{SystemSettingsService, AccountService, RepositoryService}
 | 
					import service.{SystemSettingsService, AccountService, RepositoryService}
 | 
				
			||||||
 | 
					import model._
 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
@@ -20,7 +21,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
 | 
				
			|||||||
  def destroy(): Unit = {}
 | 
					  def destroy(): Unit = {}
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
 | 
					  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
 | 
				
			||||||
    val request  = req.asInstanceOf[HttpServletRequest]
 | 
					    implicit val request  = req.asInstanceOf[HttpServletRequest]
 | 
				
			||||||
    val response = res.asInstanceOf[HttpServletResponse]
 | 
					    val response = res.asInstanceOf[HttpServletResponse]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val wrappedResponse = new HttpServletResponseWrapper(response){
 | 
					    val wrappedResponse = new HttpServletResponseWrapper(response){
 | 
				
			||||||
@@ -38,10 +39,13 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
 | 
				
			|||||||
              request.getHeader("Authorization") match {
 | 
					              request.getHeader("Authorization") match {
 | 
				
			||||||
                case null => requireAuth(response)
 | 
					                case null => requireAuth(response)
 | 
				
			||||||
                case auth => decodeAuthHeader(auth).split(":") match {
 | 
					                case auth => decodeAuthHeader(auth).split(":") match {
 | 
				
			||||||
                  case Array(username, password) if(isWritableUser(username, password, repository)) => {
 | 
					                  case Array(username, password) => getWritableUser(username, password, repository) match {
 | 
				
			||||||
                    request.setAttribute(Keys.Request.UserName, username)
 | 
					                    case Some(account) => {
 | 
				
			||||||
 | 
					                      request.setAttribute(Keys.Request.UserName, account.userName)
 | 
				
			||||||
                      chain.doFilter(req, wrappedResponse)
 | 
					                      chain.doFilter(req, wrappedResponse)
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    case None => requireAuth(response)
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
                  case _ => requireAuth(response)
 | 
					                  case _ => requireAuth(response)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
@@ -61,10 +65,11 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
 | 
					  private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
 | 
				
			||||||
 | 
					                             (implicit session: Session): Option[Account] =
 | 
				
			||||||
    authenticate(loadSystemSettings(), username, password) match {
 | 
					    authenticate(loadSystemSettings(), username, password) match {
 | 
				
			||||||
      case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
 | 
					      case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
 | 
				
			||||||
      case None => false
 | 
					      case _ => None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def requireAuth(response: HttpServletResponse): Unit = {
 | 
					  private def requireAuth(response: HttpServletResponse): Unit = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,14 +8,16 @@ import org.slf4j.LoggerFactory
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import javax.servlet.ServletConfig
 | 
					import javax.servlet.ServletConfig
 | 
				
			||||||
import javax.servlet.ServletContext
 | 
					import javax.servlet.ServletContext
 | 
				
			||||||
import javax.servlet.http.HttpServletRequest
 | 
					import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 | 
				
			||||||
import util.{Keys, JGitUtil, Directory}
 | 
					import util.{StringUtil, Keys, JGitUtil, Directory}
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import util.Implicits._
 | 
					import util.Implicits._
 | 
				
			||||||
import service._
 | 
					import service._
 | 
				
			||||||
import WebHookService._
 | 
					import WebHookService._
 | 
				
			||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import util.JGitUtil.CommitInfo
 | 
					import util.JGitUtil.CommitInfo
 | 
				
			||||||
 | 
					import service.IssuesService.IssueSearchCondition
 | 
				
			||||||
 | 
					import model.Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides Git repository via HTTP.
 | 
					 * Provides Git repository via HTTP.
 | 
				
			||||||
@@ -23,7 +25,7 @@ import util.JGitUtil.CommitInfo
 | 
				
			|||||||
 * This servlet provides only Git repository functionality.
 | 
					 * This servlet provides only Git repository functionality.
 | 
				
			||||||
 * Authentication is provided by [[servlet.BasicAuthenticationFilter]].
 | 
					 * Authentication is provided by [[servlet.BasicAuthenticationFilter]].
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class GitRepositoryServlet extends GitServlet {
 | 
					class GitRepositoryServlet extends GitServlet with SystemSettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet])
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
@@ -43,33 +45,49 @@ class GitRepositoryServlet extends GitServlet {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      def getServletContext(): ServletContext = config.getServletContext
 | 
					      def getServletContext(): ServletContext = config.getServletContext
 | 
				
			||||||
      def getServletName(): String = config.getServletName
 | 
					      def getServletName(): String = config.getServletName
 | 
				
			||||||
    });
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super.init(config)
 | 
					    super.init(config)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = {
 | 
				
			||||||
 | 
					    val agent = req.getHeader("USER-AGENT")
 | 
				
			||||||
 | 
					    val index = req.getRequestURI.indexOf(".git")
 | 
				
			||||||
 | 
					    if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){
 | 
				
			||||||
 | 
					      // redirect for browsers
 | 
				
			||||||
 | 
					      val paths = req.getRequestURI.substring(0, index).split("/")
 | 
				
			||||||
 | 
					      res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // response for git client
 | 
				
			||||||
 | 
					      super.service(req, res)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] {
 | 
					class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
 | 
					  override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
 | 
				
			||||||
    val receivePack = new ReceivePack(db)
 | 
					    val receivePack = new ReceivePack(db)
 | 
				
			||||||
    val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
 | 
					    val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.debug("requestURI: " + request.getRequestURI)
 | 
					    logger.debug("requestURI: " + request.getRequestURI)
 | 
				
			||||||
    logger.debug("userName:" + userName)
 | 
					    logger.debug("pusher:" + pusher)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    defining(request.paths){ paths =>
 | 
					    defining(request.paths){ paths =>
 | 
				
			||||||
      val owner      = paths(1)
 | 
					      val owner      = paths(1)
 | 
				
			||||||
      val repository = paths(2).replaceFirst("\\.git$", "")
 | 
					      val repository = paths(2).stripSuffix(".git")
 | 
				
			||||||
      val baseURL    = request.getRequestURL.toString.replaceFirst("/git/.*", "")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      logger.debug("repository:" + owner + "/" + repository)
 | 
					      logger.debug("repository:" + owner + "/" + repository)
 | 
				
			||||||
      logger.debug("baseURL:" + baseURL)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
 | 
					      if(!repository.endsWith(".wiki")){
 | 
				
			||||||
 | 
					        defining(request) { implicit r =>
 | 
				
			||||||
 | 
					          val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
 | 
				
			||||||
 | 
					          receivePack.setPreReceiveHook(hook)
 | 
				
			||||||
 | 
					          receivePack.setPostReceiveHook(hook)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      receivePack
 | 
					      receivePack
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -77,84 +95,142 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import scala.collection.JavaConverters._
 | 
					import scala.collection.JavaConverters._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
 | 
					class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
 | 
				
			||||||
  with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
 | 
					  extends PostReceiveHook with PreReceiveHook
 | 
				
			||||||
 | 
					  with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService {
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
 | 
					  private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
 | 
				
			||||||
 | 
					  private var existIds: Seq[String] = Nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
 | 
				
			||||||
 | 
					        existIds = JGitUtil.getAllCommitIds(git)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      case ex: Exception => {
 | 
				
			||||||
 | 
					        logger.error(ex.toString, ex)
 | 
				
			||||||
 | 
					        throw ex
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
 | 
					  def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
      using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
 | 
					      using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
 | 
				
			||||||
 | 
					        val pushedIds = scala.collection.mutable.Set[String]()
 | 
				
			||||||
        commands.asScala.foreach { command =>
 | 
					        commands.asScala.foreach { command =>
 | 
				
			||||||
          logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
 | 
					          logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
 | 
				
			||||||
        val commits = command.getType match {
 | 
					          val refName = command.getRefName.split("/")
 | 
				
			||||||
 | 
					          val branchName = refName.drop(2).mkString("/")
 | 
				
			||||||
 | 
					          val commits = if (refName(1) == "tags") {
 | 
				
			||||||
 | 
					            Nil
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            command.getType match {
 | 
				
			||||||
              case ReceiveCommand.Type.DELETE => Nil
 | 
					              case ReceiveCommand.Type.DELETE => Nil
 | 
				
			||||||
              case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
 | 
					              case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        val refName = command.getRefName.split("/")
 | 
					          }
 | 
				
			||||||
        val branchName = refName.drop(2).mkString("/")
 | 
					
 | 
				
			||||||
 | 
					          // Retrieve all issue count in the repository
 | 
				
			||||||
 | 
					          val issueCount =
 | 
				
			||||||
 | 
					            countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
 | 
				
			||||||
 | 
					            countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Extract new commit and apply issue comment
 | 
					          // Extract new commit and apply issue comment
 | 
				
			||||||
        val newCommits = if(commits.size > 1000){
 | 
					          val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
 | 
				
			||||||
          val existIds = getAllCommitIds(owner, repository)
 | 
					          val newCommits = commits.flatMap { commit =>
 | 
				
			||||||
          commits.flatMap { commit =>
 | 
					            if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
 | 
				
			||||||
            optionIf(!existIds.contains(commit.id)){
 | 
					              if (issueCount > 0) {
 | 
				
			||||||
 | 
					                pushedIds.add(commit.id)
 | 
				
			||||||
                createIssueComment(commit)
 | 
					                createIssueComment(commit)
 | 
				
			||||||
 | 
					                // close issues
 | 
				
			||||||
 | 
					                if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
 | 
				
			||||||
 | 
					                  closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
              Some(commit)
 | 
					              Some(commit)
 | 
				
			||||||
 | 
					            } else None
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } 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
 | 
					          // record activity
 | 
				
			||||||
          if(refName(1) == "heads"){
 | 
					          if(refName(1) == "heads"){
 | 
				
			||||||
            command.getType match {
 | 
					            command.getType match {
 | 
				
			||||||
            case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, userName, branchName)
 | 
					              case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
 | 
				
			||||||
            case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
 | 
					              case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
 | 
				
			||||||
            case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, userName, branchName)
 | 
					              case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
 | 
				
			||||||
              case _ =>
 | 
					              case _ =>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } else if(refName(1) == "tags"){
 | 
					          } else if(refName(1) == "tags"){
 | 
				
			||||||
            command.getType match {
 | 
					            command.getType match {
 | 
				
			||||||
            case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
 | 
					              case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
 | 
				
			||||||
            case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, userName, branchName, newCommits)
 | 
					              case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
 | 
				
			||||||
 | 
					              case _ =>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if(refName(1) == "heads"){
 | 
				
			||||||
 | 
					            command.getType match {
 | 
				
			||||||
 | 
					              case ReceiveCommand.Type.CREATE |
 | 
				
			||||||
 | 
					                   ReceiveCommand.Type.UPDATE |
 | 
				
			||||||
 | 
					                   ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
 | 
				
			||||||
 | 
					                updatePullRequests(branchName)
 | 
				
			||||||
              case _ =>
 | 
					              case _ =>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // call web hook
 | 
					          // call web hook
 | 
				
			||||||
        val webHookURLs = getWebHookURLs(owner, repository)
 | 
					          getWebHookURLs(owner, repository) match {
 | 
				
			||||||
        if(webHookURLs.nonEmpty){
 | 
					            case webHookURLs if(webHookURLs.nonEmpty) =>
 | 
				
			||||||
          val payload = WebHookPayload(
 | 
					              for(pusherAccount <- getAccountByUserName(pusher);
 | 
				
			||||||
            git,
 | 
					                  ownerAccount   <- getAccountByUserName(owner);
 | 
				
			||||||
            command.getRefName,
 | 
					                  repositoryInfo <- getRepository(owner, repository, baseUrl)){
 | 
				
			||||||
            getRepository(owner, repository, baseURL).get,
 | 
					                callWebHook(owner, repository, webHookURLs,
 | 
				
			||||||
            newCommits,
 | 
					                  WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
 | 
				
			||||||
            getAccountByUserName(owner).get)
 | 
					              }
 | 
				
			||||||
 | 
					            case _ =>
 | 
				
			||||||
          callWebHook(owner, repository, webHookURLs, payload)
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // update repository last modified time.
 | 
					      // update repository last modified time.
 | 
				
			||||||
      updateLastActivityDate(owner, repository)
 | 
					      updateLastActivityDate(owner, repository)
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      case ex: Exception => {
 | 
				
			||||||
 | 
					        logger.error(ex.toString, ex)
 | 
				
			||||||
 | 
					        throw ex
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def createIssueComment(commit: CommitInfo) = {
 | 
					  private def createIssueComment(commit: CommitInfo) = {
 | 
				
			||||||
    "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
 | 
					    StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
 | 
				
			||||||
      val issueId = matchData.group(2)
 | 
					      if(getIssue(owner, repository, issueId).isDefined){
 | 
				
			||||||
      if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
 | 
					        getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
 | 
				
			||||||
        createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
 | 
					          createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def updatePullRequests(branch: String) =
 | 
				
			||||||
 | 
					    getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
 | 
				
			||||||
 | 
					      if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
 | 
				
			||||||
 | 
					        using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
 | 
				
			||||||
 | 
					              Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
 | 
				
			||||||
 | 
					          oldGit.fetch
 | 
				
			||||||
 | 
					            .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
 | 
				
			||||||
 | 
					            .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
 | 
				
			||||||
 | 
					            .call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
 | 
				
			||||||
 | 
					          val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
 | 
				
			||||||
 | 
					            pullreq.userName, pullreq.repositoryName, pullreq.branch,
 | 
				
			||||||
 | 
					            pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
 | 
				
			||||||
 | 
					          updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										177
									
								
								src/main/scala/servlet/PluginActionInvokeFilter.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/main/scala/servlet/PluginActionInvokeFilter.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,177 @@
 | 
				
			|||||||
 | 
					package servlet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.servlet._
 | 
				
			||||||
 | 
					import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 | 
				
			||||||
 | 
					import org.apache.commons.io.IOUtils
 | 
				
			||||||
 | 
					import play.twirl.api.Html
 | 
				
			||||||
 | 
					import service.{AccountService, RepositoryService, SystemSettingsService}
 | 
				
			||||||
 | 
					import model.{Account, Session}
 | 
				
			||||||
 | 
					import util.{JGitUtil, Keys}
 | 
				
			||||||
 | 
					import plugin.{Fragment, PluginConnectionHolder, Redirect}
 | 
				
			||||||
 | 
					import service.RepositoryService.RepositoryInfo
 | 
				
			||||||
 | 
					import plugin.Security._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def init(config: FilterConfig) = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy(): Unit = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
 | 
				
			||||||
 | 
					    (req, res) match {
 | 
				
			||||||
 | 
					      case (request: HttpServletRequest, response: HttpServletResponse) => {
 | 
				
			||||||
 | 
					        Database(req.getServletContext) withTransaction { implicit session =>
 | 
				
			||||||
 | 
					          val path = req.asInstanceOf[HttpServletRequest].getRequestURI
 | 
				
			||||||
 | 
					          if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
 | 
				
			||||||
 | 
					            chain.doFilter(req, res)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
 | 
				
			||||||
 | 
					                                 (implicit session: Session): Boolean = {
 | 
				
			||||||
 | 
					    plugin.PluginSystem.globalActions.find(x =>
 | 
				
			||||||
 | 
					      x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
 | 
				
			||||||
 | 
					    ).map { action =>
 | 
				
			||||||
 | 
					      val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
 | 
				
			||||||
 | 
					      val systemSettings = loadSystemSettings()
 | 
				
			||||||
 | 
					      implicit val context = app.Context(systemSettings, Option(loginAccount), request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if(authenticate(action.security, context)){
 | 
				
			||||||
 | 
					        val result = try {
 | 
				
			||||||
 | 
					          PluginConnectionHolder.threadLocal.set(session.conn)
 | 
				
			||||||
 | 
					          action.function(request, response, context)
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					          PluginConnectionHolder.threadLocal.remove()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        result match {
 | 
				
			||||||
 | 
					          case x: String   => renderGlobalHtml(request, response, context, x)
 | 
				
			||||||
 | 
					          case x: Html     => renderGlobalHtml(request, response, context, x.toString)
 | 
				
			||||||
 | 
					          case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
 | 
				
			||||||
 | 
					          case x: Redirect => response.sendRedirect(x.path)
 | 
				
			||||||
 | 
					          case x: AnyRef   => renderJson(request, response, x)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // TODO NotFound or Error?
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					    } getOrElse false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
 | 
				
			||||||
 | 
					                                     (implicit session: Session): Boolean = {
 | 
				
			||||||
 | 
					    val elements = path.split("/")
 | 
				
			||||||
 | 
					    if(elements.length > 3){
 | 
				
			||||||
 | 
					      val owner  = elements(1)
 | 
				
			||||||
 | 
					      val name   = elements(2)
 | 
				
			||||||
 | 
					      val remain = elements.drop(3).mkString("/", "/", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
 | 
				
			||||||
 | 
					      val systemSettings = loadSystemSettings()
 | 
				
			||||||
 | 
					      implicit val context = app.Context(systemSettings, Option(loginAccount), request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
 | 
				
			||||||
 | 
					        plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
 | 
				
			||||||
 | 
					          if(authenticate(action.security, context, repository)){
 | 
				
			||||||
 | 
					            val result = try {
 | 
				
			||||||
 | 
					              PluginConnectionHolder.threadLocal.set(session.conn)
 | 
				
			||||||
 | 
					              action.function(request, response, context, repository)
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					              PluginConnectionHolder.threadLocal.remove()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            result match {
 | 
				
			||||||
 | 
					              case x: String   => renderRepositoryHtml(request, response, context, repository, x)
 | 
				
			||||||
 | 
					              case x: Html     => renderGlobalHtml(request, response, context, x.toString)
 | 
				
			||||||
 | 
					              case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
 | 
				
			||||||
 | 
					              case x: Redirect => response.sendRedirect(x.path)
 | 
				
			||||||
 | 
					              case x: AnyRef   => renderJson(request, response, x)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // TODO NotFound or Error?
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } getOrElse false
 | 
				
			||||||
 | 
					    } else false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Authentication for global action
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
 | 
				
			||||||
 | 
					    // Global Action
 | 
				
			||||||
 | 
					    security match {
 | 
				
			||||||
 | 
					      case All() => true
 | 
				
			||||||
 | 
					      case Login() => context.loginAccount.isDefined
 | 
				
			||||||
 | 
					      case Admin() => context.loginAccount.exists(_.isAdmin)
 | 
				
			||||||
 | 
					      case _ => false // TODO throw Exception?
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Authenticate for repository action
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
 | 
				
			||||||
 | 
					    if(repository.repository.isPrivate){
 | 
				
			||||||
 | 
					      // Private Repository
 | 
				
			||||||
 | 
					      security match {
 | 
				
			||||||
 | 
					        case Admin() => context.loginAccount.exists(_.isAdmin)
 | 
				
			||||||
 | 
					        case Owner() => context.loginAccount.exists { account =>
 | 
				
			||||||
 | 
					          account.userName == repository.owner ||
 | 
				
			||||||
 | 
					            getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case _ => context.loginAccount.exists { account =>
 | 
				
			||||||
 | 
					          account.isAdmin || account.userName == repository.owner ||
 | 
				
			||||||
 | 
					            getCollaborators(repository.owner, repository.name).contains(account.userName)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Public Repository
 | 
				
			||||||
 | 
					      security match {
 | 
				
			||||||
 | 
					        case All() => true
 | 
				
			||||||
 | 
					        case Login() => context.loginAccount.isDefined
 | 
				
			||||||
 | 
					        case Owner() => context.loginAccount.exists { account =>
 | 
				
			||||||
 | 
					          account.userName == repository.owner ||
 | 
				
			||||||
 | 
					            getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case Member() => context.loginAccount.exists { account =>
 | 
				
			||||||
 | 
					          account.userName == repository.owner ||
 | 
				
			||||||
 | 
					            getCollaborators(repository.owner, repository.name).contains(account.userName)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case Admin() => context.loginAccount.exists(_.isAdmin)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
 | 
				
			||||||
 | 
					    response.setContentType("text/html; charset=UTF-8")
 | 
				
			||||||
 | 
					    val html = _root_.html.main("GitBucket", None)(Html(body))(context)
 | 
				
			||||||
 | 
					    IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
 | 
				
			||||||
 | 
					    response.setContentType("text/html; charset=UTF-8")
 | 
				
			||||||
 | 
					    val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
 | 
				
			||||||
 | 
					    IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
 | 
				
			||||||
 | 
					    response.setContentType("text/html; charset=UTF-8")
 | 
				
			||||||
 | 
					    IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
 | 
				
			||||||
 | 
					    import org.json4s._
 | 
				
			||||||
 | 
					    import org.json4s.jackson.Serialization
 | 
				
			||||||
 | 
					    import org.json4s.jackson.Serialization.write
 | 
				
			||||||
 | 
					    implicit val formats = Serialization.formats(NoTypeHints)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val json = write(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    response.setContentType("application/json; charset=UTF-8")
 | 
				
			||||||
 | 
					    IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
package servlet
 | 
					package servlet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
 | 
					import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
 | 
				
			||||||
import app.FileUploadControllerBase
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Removes session associated temporary files when session is destroyed.
 | 
					 * Removes session associated temporary files when session is destroyed.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
 | 
					class SessionCleanupListener extends HttpSessionListener {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def sessionCreated(se: HttpSessionEvent): Unit = {}
 | 
					  def sessionCreated(se: HttpSessionEvent): Unit = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
 | 
					  def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ package servlet
 | 
				
			|||||||
import javax.servlet._
 | 
					import javax.servlet._
 | 
				
			||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import javax.servlet.http.HttpServletRequest
 | 
					import javax.servlet.http.HttpServletRequest
 | 
				
			||||||
 | 
					import util.Keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Controls the transaction with the open session in view pattern.
 | 
					 * Controls the transaction with the open session in view pattern.
 | 
				
			||||||
@@ -20,8 +21,9 @@ class TransactionFilter extends Filter {
 | 
				
			|||||||
      // assets don't need transaction
 | 
					      // assets don't need transaction
 | 
				
			||||||
      chain.doFilter(req, res)
 | 
					      chain.doFilter(req, res)
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      Database(req.getServletContext) withTransaction {
 | 
					      Database(req.getServletContext) withTransaction { session =>
 | 
				
			||||||
        logger.debug("begin transaction")
 | 
					        logger.debug("begin transaction")
 | 
				
			||||||
 | 
					        req.setAttribute(Keys.Request.DBSession, session)
 | 
				
			||||||
        chain.doFilter(req, res)
 | 
					        chain.doFilter(req, res)
 | 
				
			||||||
        logger.debug("end transaction")
 | 
					        logger.debug("end transaction")
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -31,8 +33,13 @@ class TransactionFilter extends Filter {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Database {
 | 
					object Database {
 | 
				
			||||||
  def apply(context: ServletContext): scala.slick.session.Database =
 | 
					
 | 
				
			||||||
    scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
 | 
					  def apply(context: ServletContext): slick.jdbc.JdbcBackend.Database =
 | 
				
			||||||
 | 
					    slick.jdbc.JdbcBackend.Database.forURL(context.getInitParameter("db.url"),
 | 
				
			||||||
        context.getInitParameter("db.user"),
 | 
					        context.getInitParameter("db.user"),
 | 
				
			||||||
        context.getInitParameter("db.password"))
 | 
					        context.getInitParameter("db.password"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getSession(req: ServletRequest): slick.jdbc.JdbcBackend#Session =
 | 
				
			||||||
 | 
					    req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										133
									
								
								src/main/scala/ssh/GitCommand.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/main/scala/ssh/GitCommand.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					package ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import java.io.{InputStream, OutputStream}
 | 
				
			||||||
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
 | 
					import util.Directory._
 | 
				
			||||||
 | 
					import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
 | 
				
			||||||
 | 
					import org.apache.sshd.server.command.UnknownCommand
 | 
				
			||||||
 | 
					import servlet.{Database, CommitLogHook}
 | 
				
			||||||
 | 
					import service.{AccountService, RepositoryService, SystemSettingsService}
 | 
				
			||||||
 | 
					import org.eclipse.jgit.errors.RepositoryNotFoundException
 | 
				
			||||||
 | 
					import javax.servlet.ServletContext
 | 
				
			||||||
 | 
					import model.Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object GitCommand {
 | 
				
			||||||
 | 
					  val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class GitCommand(val context: ServletContext, val owner: String, val repoName: String) extends Command {
 | 
				
			||||||
 | 
					  self: RepositoryService with AccountService =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(classOf[GitCommand])
 | 
				
			||||||
 | 
					  protected var err: OutputStream = null
 | 
				
			||||||
 | 
					  protected var in: InputStream = null
 | 
				
			||||||
 | 
					  protected var out: OutputStream = null
 | 
				
			||||||
 | 
					  protected var callback: ExitCallback = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected def runTask(user: String)(implicit session: Session): Unit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def newTask(user: String): Runnable = new Runnable {
 | 
				
			||||||
 | 
					    override def run(): Unit = {
 | 
				
			||||||
 | 
					      Database(context) withSession { implicit session =>
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          runTask(user)
 | 
				
			||||||
 | 
					          callback.onExit(0)
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					          case e: RepositoryNotFoundException =>
 | 
				
			||||||
 | 
					            logger.info(e.getMessage)
 | 
				
			||||||
 | 
					            callback.onExit(1, "Repository Not Found")
 | 
				
			||||||
 | 
					          case e: Throwable =>
 | 
				
			||||||
 | 
					            logger.error(e.getMessage, e)
 | 
				
			||||||
 | 
					            callback.onExit(1)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def start(env: Environment): Unit = {
 | 
				
			||||||
 | 
					    val user = env.getEnv.get("USER")
 | 
				
			||||||
 | 
					    val thread = new Thread(newTask(user))
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def destroy(): Unit = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def setExitCallback(callback: ExitCallback): Unit = {
 | 
				
			||||||
 | 
					    this.callback = callback
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def setErrorStream(err: OutputStream): Unit = {
 | 
				
			||||||
 | 
					    this.err = err
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def setOutputStream(out: OutputStream): Unit = {
 | 
				
			||||||
 | 
					    this.out = out
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def setInputStream(in: InputStream): Unit = {
 | 
				
			||||||
 | 
					    this.in = in
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
 | 
				
			||||||
 | 
					                              (implicit session: Session): Boolean =
 | 
				
			||||||
 | 
					    getAccountByUserName(username) match {
 | 
				
			||||||
 | 
					      case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account))
 | 
				
			||||||
 | 
					      case None => false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GitUploadPack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
 | 
				
			||||||
 | 
					    with RepositoryService with AccountService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override protected def runTask(user: String)(implicit session: Session): Unit = {
 | 
				
			||||||
 | 
					    getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
 | 
				
			||||||
 | 
					      if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
 | 
				
			||||||
 | 
					        using(Git.open(getRepositoryDir(owner, repoName))) { git =>
 | 
				
			||||||
 | 
					          val repository = git.getRepository
 | 
				
			||||||
 | 
					          val upload = new UploadPack(repository)
 | 
				
			||||||
 | 
					          upload.upload(in, out, err)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GitReceivePack(context: ServletContext, owner: String, repoName: String, baseUrl: String) extends GitCommand(context, owner, repoName)
 | 
				
			||||||
 | 
					    with SystemSettingsService with RepositoryService with AccountService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override protected def runTask(user: String)(implicit session: Session): Unit = {
 | 
				
			||||||
 | 
					    getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
 | 
				
			||||||
 | 
					      if(isWritableUser(user, repositoryInfo)){
 | 
				
			||||||
 | 
					        using(Git.open(getRepositoryDir(owner, repoName))) { git =>
 | 
				
			||||||
 | 
					          val repository = git.getRepository
 | 
				
			||||||
 | 
					          val receive = new ReceivePack(repository)
 | 
				
			||||||
 | 
					          if(!repoName.endsWith(".wiki")){
 | 
				
			||||||
 | 
					            val hook = new CommitLogHook(owner, repoName, user, baseUrl)
 | 
				
			||||||
 | 
					            receive.setPreReceiveHook(hook)
 | 
				
			||||||
 | 
					            receive.setPostReceiveHook(hook)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          receive.receive(in, out, err)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GitCommandFactory(context: ServletContext, baseUrl: String) extends CommandFactory {
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def createCommand(command: String): Command = {
 | 
				
			||||||
 | 
					    logger.debug(s"command: $command")
 | 
				
			||||||
 | 
					    command match {
 | 
				
			||||||
 | 
					      case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(context, owner, repoName, baseUrl)
 | 
				
			||||||
 | 
					      case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(context, owner, repoName, baseUrl)
 | 
				
			||||||
 | 
					      case _ => new UnknownCommand(command)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								src/main/scala/ssh/NoShell.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/main/scala/ssh/NoShell.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					package ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.sshd.common.Factory
 | 
				
			||||||
 | 
					import org.apache.sshd.server.{Environment, ExitCallback, Command}
 | 
				
			||||||
 | 
					import java.io.{OutputStream, InputStream}
 | 
				
			||||||
 | 
					import org.eclipse.jgit.lib.Constants
 | 
				
			||||||
 | 
					import service.SystemSettingsService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NoShell extends Factory[Command] with SystemSettingsService {
 | 
				
			||||||
 | 
					  override def create(): Command = new Command() {
 | 
				
			||||||
 | 
					    private var in: InputStream = null
 | 
				
			||||||
 | 
					    private var out: OutputStream = null
 | 
				
			||||||
 | 
					    private var err: OutputStream = null
 | 
				
			||||||
 | 
					    private var callback: ExitCallback = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def start(env: Environment): Unit = {
 | 
				
			||||||
 | 
					      val user = env.getEnv.get("USER")
 | 
				
			||||||
 | 
					      val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
 | 
				
			||||||
 | 
					      val message =
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					          | Welcome to
 | 
				
			||||||
 | 
					          |   _____   _   _     ____                   _             _
 | 
				
			||||||
 | 
					          |  / ____| (_) | |   |  _ \                 | |           | |
 | 
				
			||||||
 | 
					          | | |  __   _  | |_  | |_) |  _   _    ___  | | __   ___  | |_
 | 
				
			||||||
 | 
					          | | | |_ | | | | __| |  _ <  | | | |  / __| | |/ /  / _ \ | __|
 | 
				
			||||||
 | 
					          | | |__| | | | | |_  | |_) | | |_| | | (__  |   <  |  __/ | |_
 | 
				
			||||||
 | 
					          |  \_____| |_|  \__| |____/   \__,_|  \___| |_|\_\  \___|  \__|
 | 
				
			||||||
 | 
					          |
 | 
				
			||||||
 | 
					          | Successfully SSH Access.
 | 
				
			||||||
 | 
					          | But interactive shell is disabled.
 | 
				
			||||||
 | 
					          |
 | 
				
			||||||
 | 
					          | Please use:
 | 
				
			||||||
 | 
					          |
 | 
				
			||||||
 | 
					          | git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
 | 
				
			||||||
 | 
					        """.stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
 | 
				
			||||||
 | 
					      err.write(Constants.encode(message))
 | 
				
			||||||
 | 
					      err.flush()
 | 
				
			||||||
 | 
					      in.close()
 | 
				
			||||||
 | 
					      out.close()
 | 
				
			||||||
 | 
					      err.close()
 | 
				
			||||||
 | 
					      callback.onExit(127)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def destroy(): Unit = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def setInputStream(in: InputStream): Unit = {
 | 
				
			||||||
 | 
					      this.in = in
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def setOutputStream(out: OutputStream): Unit = {
 | 
				
			||||||
 | 
					      this.out = out
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def setErrorStream(err: OutputStream): Unit = {
 | 
				
			||||||
 | 
					      this.err = err
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override def setExitCallback(callback: ExitCallback): Unit = {
 | 
				
			||||||
 | 
					      this.callback = callback
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/main/scala/ssh/PublicKeyAuthenticator.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/main/scala/ssh/PublicKeyAuthenticator.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					package ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.sshd.server.PublickeyAuthenticator
 | 
				
			||||||
 | 
					import org.apache.sshd.server.session.ServerSession
 | 
				
			||||||
 | 
					import java.security.PublicKey
 | 
				
			||||||
 | 
					import service.SshKeyService
 | 
				
			||||||
 | 
					import servlet.Database
 | 
				
			||||||
 | 
					import javax.servlet.ServletContext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PublicKeyAuthenticator(context: ServletContext) extends PublickeyAuthenticator with SshKeyService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
 | 
				
			||||||
 | 
					    Database(context) withSession { implicit session =>
 | 
				
			||||||
 | 
					      getPublicKeys(username).exists { sshKey =>
 | 
				
			||||||
 | 
					        SshUtil.str2PublicKey(sshKey.publicKey) match {
 | 
				
			||||||
 | 
					          case Some(publicKey) => key.equals(publicKey)
 | 
				
			||||||
 | 
					          case _ => false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								src/main/scala/ssh/SshServerListener.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/main/scala/ssh/SshServerListener.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					package ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.servlet.{ServletContext, ServletContextEvent, ServletContextListener}
 | 
				
			||||||
 | 
					import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import util.Directory
 | 
				
			||||||
 | 
					import service.SystemSettingsService
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicBoolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object SshServer {
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(SshServer.getClass)
 | 
				
			||||||
 | 
					  private val server = org.apache.sshd.SshServer.setUpDefaultServer()
 | 
				
			||||||
 | 
					  private val active = new AtomicBoolean(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def configure(context: ServletContext, port: Int, baseUrl: String) = {
 | 
				
			||||||
 | 
					    server.setPort(port)
 | 
				
			||||||
 | 
					    server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser"))
 | 
				
			||||||
 | 
					    server.setPublickeyAuthenticator(new PublicKeyAuthenticator(context))
 | 
				
			||||||
 | 
					    server.setCommandFactory(new GitCommandFactory(context, baseUrl))
 | 
				
			||||||
 | 
					    server.setShellFactory(new NoShell)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start(context: ServletContext, port: Int, baseUrl: String) = {
 | 
				
			||||||
 | 
					    if(active.compareAndSet(false, true)){
 | 
				
			||||||
 | 
					      configure(context, port, baseUrl)
 | 
				
			||||||
 | 
					      server.start()
 | 
				
			||||||
 | 
					      logger.info(s"Start SSH Server Listen on ${server.getPort}")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def stop() = {
 | 
				
			||||||
 | 
					    if(active.compareAndSet(true, false)){
 | 
				
			||||||
 | 
					      server.stop(true)
 | 
				
			||||||
 | 
					      logger.info("SSH Server is stopped.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def isActive = active.get
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Start a SSH Server Daemon
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * How to use:
 | 
				
			||||||
 | 
					 * git clone ssh://username@host_or_ip:29418/owner/repository_name.git
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class SshServerListener extends ServletContextListener with SystemSettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(classOf[SshServerListener])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def contextInitialized(sce: ServletContextEvent): Unit = {
 | 
				
			||||||
 | 
					    val settings = loadSystemSettings()
 | 
				
			||||||
 | 
					    if(settings.ssh){
 | 
				
			||||||
 | 
					      settings.baseUrl match {
 | 
				
			||||||
 | 
					        case None =>
 | 
				
			||||||
 | 
					          logger.error("Could not start SshServer because the baseUrl is not configured.")
 | 
				
			||||||
 | 
					        case Some(baseUrl) =>
 | 
				
			||||||
 | 
					          SshServer.start(sce.getServletContext,
 | 
				
			||||||
 | 
					            settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def contextDestroyed(sce: ServletContextEvent): Unit = {
 | 
				
			||||||
 | 
					    if(loadSystemSettings().ssh){
 | 
				
			||||||
 | 
					      SshServer.stop()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/main/scala/ssh/SshUtil.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/main/scala/ssh/SshUtil.scala
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package ssh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.security.PublicKey
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					import org.apache.commons.codec.binary.Base64
 | 
				
			||||||
 | 
					import org.eclipse.jgit.lib.Constants
 | 
				
			||||||
 | 
					import org.apache.sshd.common.util.{KeyUtils, Buffer}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object SshUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(SshUtil.getClass)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def str2PublicKey(key: String): Option[PublicKey] = {
 | 
				
			||||||
 | 
					    // TODO RFC 4716 Public Key is not supported...
 | 
				
			||||||
 | 
					    val parts = key.split(" ")
 | 
				
			||||||
 | 
					    if (parts.size < 2) {
 | 
				
			||||||
 | 
					      logger.debug(s"Invalid PublicKey Format: ${key}")
 | 
				
			||||||
 | 
					      return None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      val encodedKey = parts(1)
 | 
				
			||||||
 | 
					      val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey))
 | 
				
			||||||
 | 
					      Some(new Buffer(decode).getRawPublicKey)
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      case e: Throwable =>
 | 
				
			||||||
 | 
					        logger.debug(e.getMessage, e)
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def fingerPrint(key: String): Option[String] = str2PublicKey(key) match {
 | 
				
			||||||
 | 
					    case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
 | 
				
			||||||
 | 
					    case None => None
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -29,7 +29,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Allows only the repository owner and administrators.
 | 
					 * Allows only the repository owner and administrators.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
 | 
					trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
 | 
				
			||||||
  protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
 | 
					  protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
 | 
				
			||||||
  protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
 | 
					  protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,6 +40,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService =>
 | 
				
			|||||||
          context.loginAccount match {
 | 
					          context.loginAccount match {
 | 
				
			||||||
            case Some(x) if(x.isAdmin) => action(repository)
 | 
					            case Some(x) if(x.isAdmin) => action(repository)
 | 
				
			||||||
            case Some(x) if(repository.owner == x.userName) => action(repository)
 | 
					            case Some(x) if(repository.owner == x.userName) => action(repository)
 | 
				
			||||||
 | 
					            case Some(x) if(getGroupMembers(repository.owner).exists { member =>
 | 
				
			||||||
 | 
					              member.userName == x.userName && member.isManager == true
 | 
				
			||||||
 | 
					            }) => action(repository)
 | 
				
			||||||
            case _ => Unauthorized()
 | 
					            case _ => Unauthorized()
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } getOrElse NotFound()
 | 
					        } getOrElse NotFound()
 | 
				
			||||||
@@ -106,7 +109,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Allows only the repository owner and administrators.
 | 
					 * Allows only the repository owner (or manager for group repository) and administrators.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
 | 
					trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
 | 
				
			||||||
  protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
 | 
					  protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
 | 
				
			||||||
@@ -155,3 +158,24 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Allows only the group managers.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					trait GroupManagerAuthenticator { self: ControllerBase with AccountService =>
 | 
				
			||||||
 | 
					  protected def managersOnly(action: => Any) = { authenticate(action) }
 | 
				
			||||||
 | 
					  protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def authenticate(action: => Any) = {
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      defining(request.paths){ paths =>
 | 
				
			||||||
 | 
					        context.loginAccount match {
 | 
				
			||||||
 | 
					          case Some(x) if(getGroupMembers(paths(0)).exists { member =>
 | 
				
			||||||
 | 
					            member.userName == x.userName && member.isManager
 | 
				
			||||||
 | 
					          }) => action
 | 
				
			||||||
 | 
					          case _ => Unauthorized()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ package util
 | 
				
			|||||||
import org.eclipse.jgit.api.Git
 | 
					import org.eclipse.jgit.api.Git
 | 
				
			||||||
import org.eclipse.jgit.revwalk.RevWalk
 | 
					import org.eclipse.jgit.revwalk.RevWalk
 | 
				
			||||||
import org.eclipse.jgit.treewalk.TreeWalk
 | 
					import org.eclipse.jgit.treewalk.TreeWalk
 | 
				
			||||||
 | 
					import scala.util.control.Exception._
 | 
				
			||||||
 | 
					import scala.language.reflectiveCalls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides control facilities.
 | 
					 * Provides control facilities.
 | 
				
			||||||
@@ -14,21 +16,19 @@ object ControlUtil {
 | 
				
			|||||||
  def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
 | 
					  def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
 | 
				
			||||||
    try f(resource) finally {
 | 
					    try f(resource) finally {
 | 
				
			||||||
      if(resource != null){
 | 
					      if(resource != null){
 | 
				
			||||||
        try {
 | 
					        ignoring(classOf[Throwable]) {
 | 
				
			||||||
          resource.close()
 | 
					          resource.close()
 | 
				
			||||||
        } catch {
 | 
					 | 
				
			||||||
          case e: Throwable => // ignore
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def using[T](git: Git)(f: Git => T): T =
 | 
					  def using[T](git: Git)(f: Git => T): T =
 | 
				
			||||||
    try f(git) finally git.getRepository.close
 | 
					    try f(git) finally git.getRepository.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
 | 
					  def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
 | 
				
			||||||
    try f(git1, git2) finally {
 | 
					    try f(git1, git2) finally {
 | 
				
			||||||
      git1.getRepository.close
 | 
					      git1.getRepository.close()
 | 
				
			||||||
      git2.getRepository.close
 | 
					      git2.getRepository.close()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
 | 
					  def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
 | 
				
			||||||
@@ -37,12 +37,4 @@ object ControlUtil {
 | 
				
			|||||||
  def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
 | 
					  def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
 | 
				
			||||||
    try f(treeWalk) finally treeWalk.release()
 | 
					    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
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,15 +2,30 @@ package util
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import java.io.File
 | 
					import java.io.File
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import org.apache.commons.io.FileUtils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides directories used by GitBucket.
 | 
					 * Provides directories used by GitBucket.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
object Directory {
 | 
					object Directory {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
 | 
					  val GitBucketHome = (System.getProperty("gitbucket.home") match {
 | 
				
			||||||
 | 
					    // -Dgitbucket.home=<path>
 | 
				
			||||||
 | 
					    case path if(path != null) => new File(path)
 | 
				
			||||||
 | 
					    case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
 | 
				
			||||||
 | 
					      // environment variable GITBUCKET_HOME
 | 
				
			||||||
      case Some(env) => new File(env)
 | 
					      case Some(env) => new File(env)
 | 
				
			||||||
    case None => new File(System.getProperty("user.home"), "gitbucket")
 | 
					      // default is HOME/.gitbucket
 | 
				
			||||||
 | 
					      case None => {
 | 
				
			||||||
 | 
					        val oldHome = new File(System.getProperty("user.home"), "gitbucket")
 | 
				
			||||||
 | 
					        if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){
 | 
				
			||||||
 | 
					          //FileUtils.moveDirectory(oldHome, newHome)
 | 
				
			||||||
 | 
					          oldHome
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          new File(System.getProperty("user.home"), ".gitbucket")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }).getAbsolutePath
 | 
					  }).getAbsolutePath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
 | 
					  val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
 | 
				
			||||||
@@ -19,19 +34,9 @@ object Directory {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  val DatabaseHome = s"${GitBucketHome}/data"
 | 
					  val DatabaseHome = s"${GitBucketHome}/data"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  val PluginHome = s"${GitBucketHome}/plugins"
 | 
				
			||||||
   * Repository names of the specified user.
 | 
					
 | 
				
			||||||
   */
 | 
					  val TemporaryHome = s"${GitBucketHome}/tmp"
 | 
				
			||||||
  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.
 | 
					   * Substance directory of the repository.
 | 
				
			||||||
@@ -39,16 +44,33 @@ object Directory {
 | 
				
			|||||||
  def getRepositoryDir(owner: String, repository: String): File =
 | 
					  def getRepositoryDir(owner: String, repository: String): File =
 | 
				
			||||||
    new File(s"${RepositoryHome}/${owner}/${repository}.git")
 | 
					    new File(s"${RepositoryHome}/${owner}/${repository}.git")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Directory for files which are attached to issue.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getAttachedDir(owner: String, repository: String): File =
 | 
				
			||||||
 | 
					    new File(s"${RepositoryHome}/${owner}/${repository}/issues")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Directory for uploaded files by the specified user.
 | 
					   * Directory for uploaded files by the specified user.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
 | 
					  def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Root of temporary directories for the upload file.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getTemporaryDir(sessionId: String): File =
 | 
				
			||||||
 | 
					    new File(s"${TemporaryHome}/_upload/${sessionId}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Root of temporary directories for the specified repository.
 | 
					   * Root of temporary directories for the specified repository.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getTemporaryDir(owner: String, repository: String): File =
 | 
					  def getTemporaryDir(owner: String, repository: String): File =
 | 
				
			||||||
    new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
 | 
					    new File(s"${TemporaryHome}/${owner}/${repository}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Root of plugin cache directory. Plugin repositories are cloned into this directory.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Temporary directory which is used to create an archive to download repository contents.
 | 
					   * Temporary directory which is used to create an archive to download repository contents.
 | 
				
			||||||
@@ -56,25 +78,10 @@ object Directory {
 | 
				
			|||||||
  def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = 
 | 
					  def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = 
 | 
				
			||||||
    new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
 | 
					    new File(getTemporaryDir(owner, repository), s"download/${sessionId}")
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Temporary directory which is used in the repository creation.
 | 
					 | 
				
			||||||
   *
 | 
					 | 
				
			||||||
   * GitBucket generates initial repository contents in this directory and push them.
 | 
					 | 
				
			||||||
   * This directory is removed after the repository creation.
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  def getInitRepositoryDir(owner: String, repository: String): File =
 | 
					 | 
				
			||||||
    new File(getTemporaryDir(owner, repository), "init")
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Substance directory of the wiki repository.
 | 
					   * Substance directory of the wiki repository.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getWikiRepositoryDir(owner: String, repository: String): File =
 | 
					  def getWikiRepositoryDir(owner: String, repository: String): File =
 | 
				
			||||||
    new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
 | 
					    new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Wiki working directory which is cloned from the wiki repository.
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  def getWikiWorkDir(owner: String, repository: String): File =
 | 
					 | 
				
			||||||
    new File(getTemporaryDir(owner, repository), "wiki")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils
 | 
				
			|||||||
import java.net.URLConnection
 | 
					import java.net.URLConnection
 | 
				
			||||||
import java.io.File
 | 
					import java.io.File
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import scala.util.Random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object FileUtil {
 | 
					object FileUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,27 +32,7 @@ object FileUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def isText(content: Array[Byte]): Boolean = !content.contains(0)
 | 
					  def isText(content: Array[Byte]): Boolean = !content.contains(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//  def createZipFile(dest: File, dir: File): Unit = {
 | 
					  def generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString
 | 
				
			||||||
//    def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = {
 | 
					 | 
				
			||||||
//      dir.listFiles.map { file =>
 | 
					 | 
				
			||||||
//        if(file.isFile){
 | 
					 | 
				
			||||||
//          out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName))
 | 
					 | 
				
			||||||
//          out.write(FileUtils.readFileToByteArray(file))
 | 
					 | 
				
			||||||
//          out.closeArchiveEntry
 | 
					 | 
				
			||||||
//        } else if(file.isDirectory){
 | 
					 | 
				
			||||||
//          addDirectoryToZip(out, file, path + "/" + file.getName)
 | 
					 | 
				
			||||||
//        }
 | 
					 | 
				
			||||||
//      }
 | 
					 | 
				
			||||||
//    }
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
//    using(new ZipArchiveOutputStream(dest)){ out =>
 | 
					 | 
				
			||||||
//      addDirectoryToZip(out, dir, dir.getName)
 | 
					 | 
				
			||||||
//    }
 | 
					 | 
				
			||||||
//  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
 | 
					 | 
				
			||||||
    if(i >= 0) path.substring(i + 1) else path
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def getExtension(name: String): String =
 | 
					  def getExtension(name: String): String =
 | 
				
			||||||
    name.lastIndexOf('.') match {
 | 
					    name.lastIndexOf('.') match {
 | 
				
			||||||
@@ -63,9 +44,9 @@ object FileUtil {
 | 
				
			|||||||
    if(dir.exists()){
 | 
					    if(dir.exists()){
 | 
				
			||||||
      FileUtils.deleteDirectory(dir)
 | 
					      FileUtils.deleteDirectory(dir)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    try{
 | 
					    try {
 | 
				
			||||||
      action(dir)
 | 
					      action(dir)
 | 
				
			||||||
    }finally{
 | 
					    } finally {
 | 
				
			||||||
      FileUtils.deleteDirectory(dir)
 | 
					      FileUtils.deleteDirectory(dir)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,9 @@
 | 
				
			|||||||
package util
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import scala.util.matching.Regex
 | 
					import scala.util.matching.Regex
 | 
				
			||||||
 | 
					import scala.util.control.Exception._
 | 
				
			||||||
 | 
					import slick.jdbc.JdbcBackend
 | 
				
			||||||
 | 
					import servlet.Database
 | 
				
			||||||
import javax.servlet.http.{HttpSession, HttpServletRequest}
 | 
					import javax.servlet.http.{HttpSession, HttpServletRequest}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -8,6 +11,9 @@ import javax.servlet.http.{HttpSession, HttpServletRequest}
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
object Implicits {
 | 
					object Implicits {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Convert to slick session.
 | 
				
			||||||
 | 
					  implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  implicit class RichSeq[A](seq: Seq[A]) {
 | 
					  implicit class RichSeq[A](seq: Seq[A]) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
 | 
					    def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
 | 
				
			||||||
@@ -42,10 +48,8 @@ object Implicits {
 | 
				
			|||||||
      sb.toString
 | 
					      sb.toString
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def toIntOpt: Option[Int] = try {
 | 
					    def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt {
 | 
				
			||||||
      Option(Integer.parseInt(value))
 | 
					      Integer.parseInt(value)
 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      case e: NumberFormatException => None
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,16 +11,20 @@ import org.eclipse.jgit.revwalk.filter._
 | 
				
			|||||||
import org.eclipse.jgit.treewalk._
 | 
					import org.eclipse.jgit.treewalk._
 | 
				
			||||||
import org.eclipse.jgit.treewalk.filter._
 | 
					import org.eclipse.jgit.treewalk.filter._
 | 
				
			||||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
 | 
					import org.eclipse.jgit.diff.DiffEntry.ChangeType
 | 
				
			||||||
import org.eclipse.jgit.errors.MissingObjectException
 | 
					import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
 | 
				
			||||||
import java.util.Date
 | 
					import java.util.Date
 | 
				
			||||||
import org.eclipse.jgit.api.errors.NoHeadException
 | 
					import org.eclipse.jgit.api.errors.NoHeadException
 | 
				
			||||||
import service.RepositoryService
 | 
					import service.RepositoryService
 | 
				
			||||||
 | 
					import org.eclipse.jgit.dircache.DirCacheEntry
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Provides complex JGit operations.
 | 
					 * Provides complex JGit operations.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
object JGitUtil {
 | 
					object JGitUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(JGitUtil.getClass)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * The repository data.
 | 
					   * The repository data.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
@@ -31,7 +35,11 @@ object JGitUtil {
 | 
				
			|||||||
   * @param branchList the list of branch names
 | 
					   * @param branchList the list of branch names
 | 
				
			||||||
   * @param tags the list of tags
 | 
					   * @param tags the list of tags
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo])
 | 
					  case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){
 | 
				
			||||||
 | 
					    def this(owner: String, name: String, baseUrl: String) = {
 | 
				
			||||||
 | 
					      this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * The file data for the file list of the repository viewer.
 | 
					   * The file data for the file list of the repository viewer.
 | 
				
			||||||
@@ -39,49 +47,55 @@ object JGitUtil {
 | 
				
			|||||||
   * @param id the object id
 | 
					   * @param id the object id
 | 
				
			||||||
   * @param isDirectory whether is it directory
 | 
					   * @param isDirectory whether is it directory
 | 
				
			||||||
   * @param name the file (or directory) name
 | 
					   * @param name the file (or directory) name
 | 
				
			||||||
   * @param time the last modified time
 | 
					 | 
				
			||||||
   * @param message the last commit message
 | 
					   * @param message the last commit message
 | 
				
			||||||
   * @param commitId the last commit id
 | 
					   * @param commitId the last commit id
 | 
				
			||||||
   * @param committer the last committer name
 | 
					   * @param time the last modified time
 | 
				
			||||||
 | 
					   * @param author the last committer name
 | 
				
			||||||
   * @param mailAddress the committer's mail address
 | 
					   * @param mailAddress the committer's mail address
 | 
				
			||||||
 | 
					   * @param linkUrl the url of submodule
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String,
 | 
					  case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String,
 | 
				
			||||||
                      committer: String, mailAddress: String)
 | 
					                      time: Date, author: String, mailAddress: String, linkUrl: Option[String])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * The commit data.
 | 
					   * The commit data.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param id the commit id
 | 
					   * @param id the commit id
 | 
				
			||||||
   * @param time the commit time
 | 
					 | 
				
			||||||
   * @param committer  the committer name
 | 
					 | 
				
			||||||
   * @param mailAddress the mail address of the committer
 | 
					 | 
				
			||||||
   * @param shortMessage the short message
 | 
					   * @param shortMessage the short message
 | 
				
			||||||
   * @param fullMessage the full message
 | 
					   * @param fullMessage the full message
 | 
				
			||||||
   * @param parents the list of parent commit id
 | 
					   * @param parents the list of parent commit id
 | 
				
			||||||
 | 
					   * @param authorTime the author time
 | 
				
			||||||
 | 
					   * @param authorName the author name
 | 
				
			||||||
 | 
					   * @param authorEmailAddress the mail address of the author
 | 
				
			||||||
 | 
					   * @param commitTime the commit time
 | 
				
			||||||
 | 
					   * @param committerName  the committer name
 | 
				
			||||||
 | 
					   * @param committerEmailAddress the mail address of the committer
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String,
 | 
					  case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String],
 | 
				
			||||||
                        shortMessage: String, fullMessage: String, parents: List[String]){
 | 
					                        authorTime: Date, authorName: String, authorEmailAddress: String,
 | 
				
			||||||
 | 
					                        commitTime: Date, committerName: String, committerEmailAddress: String){
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
 | 
					    def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this(
 | 
				
			||||||
        rev.getName,
 | 
					        rev.getName,
 | 
				
			||||||
        rev.getCommitterIdent.getWhen,
 | 
					 | 
				
			||||||
        rev.getCommitterIdent.getName,
 | 
					 | 
				
			||||||
        rev.getCommitterIdent.getEmailAddress,
 | 
					 | 
				
			||||||
        rev.getShortMessage,
 | 
					        rev.getShortMessage,
 | 
				
			||||||
        rev.getFullMessage,
 | 
					        rev.getFullMessage,
 | 
				
			||||||
        rev.getParents().map(_.name).toList)
 | 
					        rev.getParents().map(_.name).toList,
 | 
				
			||||||
 | 
					        rev.getAuthorIdent.getWhen,
 | 
				
			||||||
 | 
					        rev.getAuthorIdent.getName,
 | 
				
			||||||
 | 
					        rev.getAuthorIdent.getEmailAddress,
 | 
				
			||||||
 | 
					        rev.getCommitterIdent.getWhen,
 | 
				
			||||||
 | 
					        rev.getCommitterIdent.getName,
 | 
				
			||||||
 | 
					        rev.getCommitterIdent.getEmailAddress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
 | 
					    val summary = getSummaryMessage(fullMessage, shortMessage)
 | 
				
			||||||
      defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
 | 
					 | 
				
			||||||
        if(firstLine.length > shortMessage.length) shortMessage else firstLine
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val description = defining(fullMessage.trim.indexOf("\n")){ i =>
 | 
					    val description = defining(fullMessage.trim.indexOf("\n")){ i =>
 | 
				
			||||||
      optionIf(i >= 0){
 | 
					      if(i >= 0){
 | 
				
			||||||
        Some(fullMessage.trim.substring(i).trim)
 | 
					        Some(fullMessage.trim.substring(i).trim)
 | 
				
			||||||
 | 
					      } else None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
 | 
					    def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
 | 
					  case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
 | 
				
			||||||
@@ -91,8 +105,14 @@ object JGitUtil {
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param viewType "image", "large" or "other"
 | 
					   * @param viewType "image", "large" or "other"
 | 
				
			||||||
   * @param content the string content
 | 
					   * @param content the string content
 | 
				
			||||||
 | 
					   * @param charset the character encoding
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  case class ContentInfo(viewType: String, content: Option[String])
 | 
					  case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * the line separator of this content ("LF" or "CRLF")
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * The tag data.
 | 
					   * The tag data.
 | 
				
			||||||
@@ -103,6 +123,15 @@ object JGitUtil {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  case class TagInfo(name: String, time: Date, id: String)
 | 
					  case class TagInfo(name: String, time: Date, id: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * The submodule data
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param name the module name
 | 
				
			||||||
 | 
					   * @param path the path in the repository
 | 
				
			||||||
 | 
					   * @param url the repository url of this module
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  case class SubmoduleInfo(name: String, path: String, url: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns RevCommit from the commit or tag id.
 | 
					   * Returns RevCommit from the commit or tag id.
 | 
				
			||||||
   * 
 | 
					   * 
 | 
				
			||||||
@@ -127,7 +156,7 @@ object JGitUtil {
 | 
				
			|||||||
    using(Git.open(getRepositoryDir(owner, repository))){ git =>
 | 
					    using(Git.open(getRepositoryDir(owner, repository))){ git =>
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        // get commit count
 | 
					        // get commit count
 | 
				
			||||||
        val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
 | 
					        val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        RepositoryInfo(
 | 
					        RepositoryInfo(
 | 
				
			||||||
          owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
 | 
					          owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
 | 
				
			||||||
@@ -135,12 +164,12 @@ object JGitUtil {
 | 
				
			|||||||
          commitCount,
 | 
					          commitCount,
 | 
				
			||||||
          // branches
 | 
					          // branches
 | 
				
			||||||
          git.branchList.call.asScala.map { ref =>
 | 
					          git.branchList.call.asScala.map { ref =>
 | 
				
			||||||
            ref.getName.replaceFirst("^refs/heads/", "")
 | 
					            ref.getName.stripPrefix("refs/heads/")
 | 
				
			||||||
          }.toList,
 | 
					          }.toList,
 | 
				
			||||||
          // tags
 | 
					          // tags
 | 
				
			||||||
          git.tagList.call.asScala.map { ref =>
 | 
					          git.tagList.call.asScala.map { ref =>
 | 
				
			||||||
            val revCommit = getRevCommitFromId(git, ref.getObjectId)
 | 
					            val revCommit = getRevCommitFromId(git, ref.getObjectId)
 | 
				
			||||||
            TagInfo(ref.getName.replaceFirst("^refs/tags/", ""), revCommit.getCommitterIdent.getWhen, revCommit.getName)
 | 
					            TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
 | 
				
			||||||
          }.toList
 | 
					          }.toList
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      } catch {
 | 
					      } catch {
 | 
				
			||||||
@@ -161,7 +190,7 @@ object JGitUtil {
 | 
				
			|||||||
   * @return HTML of the file list
 | 
					   * @return HTML of the file list
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
 | 
					  def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
 | 
				
			||||||
    val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
 | 
					    val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    using(new RevWalk(git.getRepository)){ revWalk =>
 | 
					    using(new RevWalk(git.getRepository)){ revWalk =>
 | 
				
			||||||
      val objectId  = git.getRepository.resolve(revision)
 | 
					      val objectId  = git.getRepository.resolve(revision)
 | 
				
			||||||
@@ -179,7 +208,7 @@ object JGitUtil {
 | 
				
			|||||||
              val targetPath = walker.getPathString
 | 
					              val targetPath = walker.getPathString
 | 
				
			||||||
              if((path + "/").startsWith(targetPath)){
 | 
					              if((path + "/").startsWith(targetPath)){
 | 
				
			||||||
                true
 | 
					                true
 | 
				
			||||||
              } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
 | 
					              } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
 | 
				
			||||||
                stopRecursive = true
 | 
					                stopRecursive = true
 | 
				
			||||||
                treeWalk.setRecursive(false)
 | 
					                treeWalk.setRecursive(false)
 | 
				
			||||||
                true
 | 
					                true
 | 
				
			||||||
@@ -194,22 +223,30 @@ object JGitUtil {
 | 
				
			|||||||
          })
 | 
					          })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        while (treeWalk.next()) {
 | 
					        while (treeWalk.next()) {
 | 
				
			||||||
          list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
 | 
					          // submodule
 | 
				
			||||||
 | 
					          val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
 | 
				
			||||||
 | 
					            getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
 | 
				
			||||||
 | 
					          } else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
 | 
					    val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
 | 
				
			||||||
    list.map { case (objectId, fileMode, path, name) =>
 | 
					    list.map { case (objectId, fileMode, path, name, linkUrl) =>
 | 
				
			||||||
 | 
					      defining(commits(path)){ commit =>
 | 
				
			||||||
        FileInfo(
 | 
					        FileInfo(
 | 
				
			||||||
          objectId,
 | 
					          objectId,
 | 
				
			||||||
        fileMode == FileMode.TREE,
 | 
					          fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
 | 
				
			||||||
          name,
 | 
					          name,
 | 
				
			||||||
        commits(path).getCommitterIdent.getWhen,
 | 
					          getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
 | 
				
			||||||
        commits(path).getShortMessage,
 | 
					          commit.getName,
 | 
				
			||||||
        commits(path).getName,
 | 
					          commit.getAuthorIdent.getWhen,
 | 
				
			||||||
        commits(path).getCommitterIdent.getName,
 | 
					          commit.getAuthorIdent.getName,
 | 
				
			||||||
        commits(path).getCommitterIdent.getEmailAddress)
 | 
					          commit.getAuthorIdent.getEmailAddress,
 | 
				
			||||||
 | 
					          linkUrl)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }.sortWith { (file1, file2) =>
 | 
					    }.sortWith { (file1, file2) =>
 | 
				
			||||||
      (file1.isDirectory, file2.isDirectory) match {
 | 
					      (file1.isDirectory, file2.isDirectory) match {
 | 
				
			||||||
        case (true , false) => true
 | 
					        case (true , false) => true
 | 
				
			||||||
@@ -219,6 +256,17 @@ object JGitUtil {
 | 
				
			|||||||
    }.toList
 | 
					    }.toList
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns the first line of the commit message.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the commit list of the specified branch.
 | 
					   * Returns the commit list of the specified branch.
 | 
				
			||||||
   * 
 | 
					   * 
 | 
				
			||||||
@@ -324,27 +372,6 @@ object JGitUtil {
 | 
				
			|||||||
    }.toMap
 | 
					    }.toMap
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Get object content of the given id as String from the Git repository.
 | 
					 | 
				
			||||||
   * 
 | 
					 | 
				
			||||||
   * @param git the Git object
 | 
					 | 
				
			||||||
   * @param id the object id
 | 
					 | 
				
			||||||
   * @param large if false then returns None for the large file
 | 
					 | 
				
			||||||
   * @return the object or None if object does not exist
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try {
 | 
					 | 
				
			||||||
    val loader = git.getRepository.getObjectDatabase.open(id)
 | 
					 | 
				
			||||||
    if(large == false && FileUtil.isLarge(loader.getSize)){
 | 
					 | 
				
			||||||
      None
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      using(git.getRepository.getObjectDatabase){ db =>
 | 
					 | 
				
			||||||
        Some(db.open(id).getBytes)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } catch {
 | 
					 | 
				
			||||||
    case e: MissingObjectException => None
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the tuple of diff of the given commit and the previous commit id.
 | 
					   * Returns the tuple of diff of the given commit and the previous commit id.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -363,7 +390,12 @@ object JGitUtil {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if(commits.length >= 2){
 | 
					      if(commits.length >= 2){
 | 
				
			||||||
        // not initial commit
 | 
					        // not initial commit
 | 
				
			||||||
        val oldCommit = commits(1)
 | 
					        val oldCommit = if(revCommit.getParentCount >= 2) {
 | 
				
			||||||
 | 
					          // merge commit
 | 
				
			||||||
 | 
					          revCommit.getParents.head
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          commits(1)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
 | 
					        (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@@ -376,7 +408,7 @@ object JGitUtil {
 | 
				
			|||||||
              DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
 | 
					              DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
 | 
					              DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
 | 
				
			||||||
                JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
 | 
					                JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
 | 
				
			||||||
            }))
 | 
					            }))
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          (buffer.toList, None)
 | 
					          (buffer.toList, None)
 | 
				
			||||||
@@ -399,8 +431,8 @@ object JGitUtil {
 | 
				
			|||||||
        DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
 | 
					        DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
 | 
					        DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
 | 
				
			||||||
          JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
 | 
					          JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
 | 
				
			||||||
          JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
 | 
					          JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }.toList
 | 
					    }.toList
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -464,4 +496,171 @@ object JGitUtil {
 | 
				
			|||||||
    }.find(_._1 != null)
 | 
					    }.find(_._1 != null)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
 | 
				
			||||||
 | 
					    val entry = new DirCacheEntry(path)
 | 
				
			||||||
 | 
					    entry.setFileMode(mode)
 | 
				
			||||||
 | 
					    entry.setObjectId(objectId)
 | 
				
			||||||
 | 
					    entry
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId,
 | 
				
			||||||
 | 
					                      ref: String, fullName: String, mailAddress: String, message: String): ObjectId = {
 | 
				
			||||||
 | 
					    val newCommit = new CommitBuilder()
 | 
				
			||||||
 | 
					    newCommit.setCommitter(new PersonIdent(fullName, mailAddress))
 | 
				
			||||||
 | 
					    newCommit.setAuthor(new PersonIdent(fullName, mailAddress))
 | 
				
			||||||
 | 
					    newCommit.setMessage(message)
 | 
				
			||||||
 | 
					    if(headId != null){
 | 
				
			||||||
 | 
					      newCommit.setParentIds(List(headId).asJava)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    newCommit.setTreeId(treeId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val newHeadId = inserter.insert(newCommit)
 | 
				
			||||||
 | 
					    inserter.flush()
 | 
				
			||||||
 | 
					    inserter.release()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val refUpdate = git.getRepository.updateRef(ref)
 | 
				
			||||||
 | 
					    refUpdate.setNewObjectId(newHeadId)
 | 
				
			||||||
 | 
					    refUpdate.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    newHeadId
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Read submodule information from .gitmodules
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
 | 
				
			||||||
 | 
					    val repository = git.getRepository
 | 
				
			||||||
 | 
					    getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
 | 
				
			||||||
 | 
					      (try {
 | 
				
			||||||
 | 
					        val config = new BlobBasedConfig(repository.getConfig(), bytes)
 | 
				
			||||||
 | 
					        config.getSubsections("submodule").asScala.map { module =>
 | 
				
			||||||
 | 
					          val path = config.getString("submodule", module, "path")
 | 
				
			||||||
 | 
					          val url  = config.getString("submodule", module, "url")
 | 
				
			||||||
 | 
					          SubmoduleInfo(module, path, url)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        case e: ConfigInvalidException => {
 | 
				
			||||||
 | 
					          logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e)
 | 
				
			||||||
 | 
					          Nil
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }).toList
 | 
				
			||||||
 | 
					    } getOrElse Nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get object content of the given path as byte array from the Git repository.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param git the Git object
 | 
				
			||||||
 | 
					   * @param revTree the rev tree
 | 
				
			||||||
 | 
					   * @param path the path
 | 
				
			||||||
 | 
					   * @param fetchLargeFile if false then returns None for the large file
 | 
				
			||||||
 | 
					   * @return the byte array of content or None if object does not exist
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = {
 | 
				
			||||||
 | 
					    @scala.annotation.tailrec
 | 
				
			||||||
 | 
					    def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
 | 
				
			||||||
 | 
					      case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
 | 
				
			||||||
 | 
					      case true  => getPathObjectId(path, walk)
 | 
				
			||||||
 | 
					      case false => None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    using(new TreeWalk(git.getRepository)){ treeWalk =>
 | 
				
			||||||
 | 
					      treeWalk.addTree(revTree)
 | 
				
			||||||
 | 
					      treeWalk.setRecursive(true)
 | 
				
			||||||
 | 
					      getPathObjectId(path, treeWalk)
 | 
				
			||||||
 | 
					    } flatMap { objectId =>
 | 
				
			||||||
 | 
					      getContentFromId(git, objectId, fetchLargeFile)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
 | 
				
			||||||
 | 
					    // Viewer
 | 
				
			||||||
 | 
					    val large  = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
 | 
				
			||||||
 | 
					    val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
 | 
				
			||||||
 | 
					    val bytes  = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if(viewer == "other"){
 | 
				
			||||||
 | 
					      if(bytes.isDefined && FileUtil.isText(bytes.get)){
 | 
				
			||||||
 | 
					        // text
 | 
				
			||||||
 | 
					        ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // binary
 | 
				
			||||||
 | 
					        ContentInfo("binary", None, None)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // image or large
 | 
				
			||||||
 | 
					      ContentInfo(viewer, None, None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get object content of the given object id as byte array from the Git repository.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param git the Git object
 | 
				
			||||||
 | 
					   * @param id the object id
 | 
				
			||||||
 | 
					   * @param fetchLargeFile if false then returns None for the large file
 | 
				
			||||||
 | 
					   * @return the byte array of content or None if object does not exist
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
 | 
				
			||||||
 | 
					    val loader = git.getRepository.getObjectDatabase.open(id)
 | 
				
			||||||
 | 
					    if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
 | 
				
			||||||
 | 
					      None
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      using(git.getRepository.getObjectDatabase){ db =>
 | 
				
			||||||
 | 
					        Some(db.open(id).getBytes)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch {
 | 
				
			||||||
 | 
					    case e: MissingObjectException => None
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns all commit id in the specified repository.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) {
 | 
				
			||||||
 | 
					    Nil
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    val existIds = new scala.collection.mutable.ListBuffer[String]()
 | 
				
			||||||
 | 
					    val i = git.log.all.call.iterator
 | 
				
			||||||
 | 
					    while(i.hasNext){
 | 
				
			||||||
 | 
					      existIds += i.next.name
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    existIds.toSeq
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = {
 | 
				
			||||||
 | 
					    using(new RevWalk(git.getRepository)){ revWalk =>
 | 
				
			||||||
 | 
					      using(new TreeWalk(git.getRepository)){ treeWalk =>
 | 
				
			||||||
 | 
					        val index = treeWalk.addTree(revWalk.parseTree(id))
 | 
				
			||||||
 | 
					        treeWalk.setRecursive(true)
 | 
				
			||||||
 | 
					        while(treeWalk.next){
 | 
				
			||||||
 | 
					          f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns the identifier of the root commit (or latest merge commit) of the specified branch.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getForkedCommitId(oldGit: Git, newGit: Git,
 | 
				
			||||||
 | 
					                        userName: String, repositoryName: String, branch: String,
 | 
				
			||||||
 | 
					                        requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
 | 
				
			||||||
 | 
					    defining(getAllCommitIds(oldGit)){ existIds =>
 | 
				
			||||||
 | 
					      getCommitLogs(newGit, requestBranch, true) { commit =>
 | 
				
			||||||
 | 
					        existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch)
 | 
				
			||||||
 | 
					      }.head.id
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns the last modified commit of specified path
 | 
				
			||||||
 | 
					   * @param git the Git object
 | 
				
			||||||
 | 
					   * @param startCommit the search base commit id
 | 
				
			||||||
 | 
					   * @param path the path of target file or directory
 | 
				
			||||||
 | 
					   * @return the last modified commit of specified path
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = {
 | 
				
			||||||
 | 
					    return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,12 +13,7 @@ object Keys {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Session key for the logged in account information.
 | 
					     * Session key for the logged in account information.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    val LoginAccount = "LOGIN_ACCOUNT"
 | 
					    val LoginAccount = "loginAccount"
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Session key for the redirect URL.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    val Redirect = "REDIRECT"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Session key for the issue search condition in dashboard.
 | 
					     * Session key for the issue search condition in dashboard.
 | 
				
			||||||
@@ -47,11 +42,30 @@ object Keys {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  object Flash {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Flash key for the redirect URL.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val Redirect = "redirect"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Flash key for the information message.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val Info = "info"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Define request keys.
 | 
					   * Define request keys.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  object Request {
 | 
					  object Request {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Request key for the Slick Session.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val DBSession = "DB_SESSION"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Request key for the Ajax request flag.
 | 
					     * Request key for the Ajax request flag.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,81 +3,146 @@ package util
 | 
				
			|||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
import service.SystemSettingsService
 | 
					import service.SystemSettingsService
 | 
				
			||||||
import com.novell.ldap._
 | 
					import com.novell.ldap._
 | 
				
			||||||
 | 
					import java.security.Security
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
import service.SystemSettingsService.Ldap
 | 
					import service.SystemSettingsService.Ldap
 | 
				
			||||||
import scala.annotation.tailrec
 | 
					import scala.annotation.tailrec
 | 
				
			||||||
 | 
					import model.Account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Utility for LDAP authentication.
 | 
					 * Utility for LDAP authentication.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
object LDAPUtil {
 | 
					object LDAPUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private val LDAP_VERSION: Int = 3
 | 
					  private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3
 | 
				
			||||||
 | 
					  private val logger = LoggerFactory.getLogger(getClass().getName())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val LDAP_DUMMY_MAL = "@ldap-devnull"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns true if mail address ends with "@ldap-devnull"
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def isDummyMailAddress(account: Account): Boolean = {
 | 
				
			||||||
 | 
					    account.mailAddress.endsWith(LDAP_DUMMY_MAL)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Creates dummy address (userName@ldap-devnull) for LDAP login.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * If  mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login.
 | 
				
			||||||
 | 
					   * GitBucket does not send any mails to this dummy address. And these users must input their mail address
 | 
				
			||||||
 | 
					   * at the first step after LDAP authentication.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def createDummyMailAddress(userName: String): String = {
 | 
				
			||||||
 | 
					    userName + LDAP_DUMMY_MAL
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Try authentication by LDAP using given configuration.
 | 
					   * Try authentication by LDAP using given configuration.
 | 
				
			||||||
   * Returns Right(mailAddress) if authentication is successful, otherwise  Left(errorMessage).
 | 
					   * Returns Right(LDAPUserInfo) if authentication is successful, otherwise  Left(errorMessage).
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
 | 
					  def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = {
 | 
				
			||||||
    bind(
 | 
					    bind(
 | 
				
			||||||
      ldapSettings.host,
 | 
					      host     = ldapSettings.host,
 | 
				
			||||||
      ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
 | 
					      port     = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
 | 
				
			||||||
      ldapSettings.bindDN.getOrElse(""),
 | 
					      dn       = ldapSettings.bindDN.getOrElse(""),
 | 
				
			||||||
      ldapSettings.bindPassword.getOrElse("")
 | 
					      password = ldapSettings.bindPassword.getOrElse(""),
 | 
				
			||||||
    ) match {
 | 
					      tls      = ldapSettings.tls.getOrElse(false),
 | 
				
			||||||
      case Some(conn) => {
 | 
					      keystore = ldapSettings.keystore.getOrElse(""),
 | 
				
			||||||
        withConnection(conn) { conn =>
 | 
					      error    = "System LDAP authentication failed."
 | 
				
			||||||
          findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
 | 
					    ){ conn =>
 | 
				
			||||||
            case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
 | 
					      findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute, ldapSettings.additionalFilterCondition) match {
 | 
				
			||||||
 | 
					        case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password)
 | 
				
			||||||
        case None         => Left("User does not exist.")
 | 
					        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] = {
 | 
					  private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = {
 | 
				
			||||||
    bind(
 | 
					    bind(
 | 
				
			||||||
      ldapSettings.host,
 | 
					      host     = ldapSettings.host,
 | 
				
			||||||
      ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
 | 
					      port     = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
 | 
				
			||||||
      userDN,
 | 
					      dn       = userDN,
 | 
				
			||||||
      password
 | 
					      password = password,
 | 
				
			||||||
    ) match {
 | 
					      tls      = ldapSettings.tls.getOrElse(false),
 | 
				
			||||||
      case Some(conn) => {
 | 
					      keystore = ldapSettings.keystore.getOrElse(""),
 | 
				
			||||||
        withConnection(conn) { conn =>
 | 
					      error    = "User LDAP Authentication Failed."
 | 
				
			||||||
          findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
 | 
					    ){ conn =>
 | 
				
			||||||
            case Some(mailAddress) => Right(mailAddress)
 | 
					      if(ldapSettings.mailAttribute.getOrElse("").isEmpty) {
 | 
				
			||||||
 | 
					        Right(LDAPUserInfo(
 | 
				
			||||||
 | 
					          userName    = userName,
 | 
				
			||||||
 | 
					          fullName    = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
 | 
				
			||||||
 | 
					            findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
 | 
				
			||||||
 | 
					          }.getOrElse(userName),
 | 
				
			||||||
 | 
					          mailAddress = createDummyMailAddress(userName)))
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match {
 | 
				
			||||||
 | 
					          case Some(mailAddress) => Right(LDAPUserInfo(
 | 
				
			||||||
 | 
					            userName    = getUserNameFromMailAddress(userName),
 | 
				
			||||||
 | 
					            fullName    = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute =>
 | 
				
			||||||
 | 
					              findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute)
 | 
				
			||||||
 | 
					            }.getOrElse(userName),
 | 
				
			||||||
 | 
					            mailAddress = mailAddress))
 | 
				
			||||||
          case None => Left("Can't find mail address.")
 | 
					          case None => Left("Can't find mail address.")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      case None => Left("User LDAP Authentication Failed.")
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def getUserNameFromMailAddress(userName: String): String = {
 | 
				
			||||||
 | 
					    (userName.indexOf('@') match {
 | 
				
			||||||
 | 
					      case i if i >= 0 => userName.substring(0, i)
 | 
				
			||||||
 | 
					      case i           => userName
 | 
				
			||||||
 | 
					    }).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
 | 
				
			||||||
 | 
					                  (f: LDAPConnection => Either[String, A]): Either[String, A] = {
 | 
				
			||||||
 | 
					    if (tls) {
 | 
				
			||||||
 | 
					      // Dynamically set Sun as the security provider
 | 
				
			||||||
 | 
					      Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (keystore.compareTo("") != 0) {
 | 
				
			||||||
 | 
					        // Dynamically set the property that JSSE uses to identify
 | 
				
			||||||
 | 
					        // the keystore that holds trusted root certificates
 | 
				
			||||||
 | 
					        System.setProperty("javax.net.ssl.trustStore", keystore)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
 | 
					    val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
 | 
				
			||||||
    val conn: LDAPConnection = new LDAPConnection
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      // Connect to the server
 | 
				
			||||||
      conn.connect(host, port)
 | 
					      conn.connect(host, port)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (tls) {
 | 
				
			||||||
 | 
					        // Secure the connection
 | 
				
			||||||
 | 
					        conn.startTLS()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Bind to the server
 | 
				
			||||||
      conn.bind(LDAP_VERSION, dn, password.getBytes)
 | 
					      conn.bind(LDAP_VERSION, dn, password.getBytes)
 | 
				
			||||||
      Some(conn)
 | 
					
 | 
				
			||||||
 | 
					      // Execute a given function and returns a its result
 | 
				
			||||||
 | 
					      f(conn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } catch {
 | 
					    } catch {
 | 
				
			||||||
      case e: Exception => {
 | 
					      case e: Exception => {
 | 
				
			||||||
        if (conn.isConnected) conn.disconnect()
 | 
					        // Provide more information if something goes wrong
 | 
				
			||||||
        None
 | 
					        logger.info("" + e)
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
 | 
					        if (conn.isConnected) {
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      f(conn)
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
          conn.disconnect()
 | 
					          conn.disconnect()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        // Returns an error message
 | 
				
			||||||
 | 
					        Left(error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
 | 
					  /**
 | 
				
			||||||
 | 
					   * Search a specified user and returns userDN if exists.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String, additionalFilterCondition: Option[String]): Option[String] = {
 | 
				
			||||||
    @tailrec
 | 
					    @tailrec
 | 
				
			||||||
    def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
 | 
					    def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
 | 
				
			||||||
      if(results.hasMore){
 | 
					      if(results.hasMore){
 | 
				
			||||||
@@ -90,15 +155,31 @@ object LDAPUtil {
 | 
				
			|||||||
        entries.flatten
 | 
					        entries.flatten
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
 | 
					
 | 
				
			||||||
 | 
					    val filterCond = additionalFilterCondition.getOrElse("") match {
 | 
				
			||||||
 | 
					      case "" => userNameAttribute + "=" + userName
 | 
				
			||||||
 | 
					      case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst {
 | 
				
			||||||
      case x => x.getDN
 | 
					      case x => x.getDN
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
 | 
					  private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] =
 | 
				
			||||||
    defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
 | 
					    defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results =>
 | 
				
			||||||
      optionIf (results.hasMore) {
 | 
					      if(results.hasMore) {
 | 
				
			||||||
        Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
 | 
					        Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
 | 
				
			||||||
 | 
					      } else None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] =
 | 
				
			||||||
 | 
					    defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results =>
 | 
				
			||||||
 | 
					      if(results.hasMore) {
 | 
				
			||||||
 | 
					        Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue)
 | 
				
			||||||
 | 
					      } else None
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
 | 
				
			|||||||
import org.slf4j.LoggerFactory
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import app.Context
 | 
					import app.Context
 | 
				
			||||||
 | 
					import model.Session
 | 
				
			||||||
import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
 | 
					import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
 | 
				
			||||||
import servlet.Database
 | 
					import servlet.Database
 | 
				
			||||||
import SystemSettingsService.Smtp
 | 
					import SystemSettingsService.Smtp
 | 
				
			||||||
@@ -15,7 +16,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
 | 
				
			|||||||
  def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
 | 
					  def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
 | 
				
			||||||
      (msg: String => String)(implicit context: Context): Unit
 | 
					      (msg: String => String)(implicit context: Context): Unit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
 | 
					  protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
        // individual repository's owner
 | 
					        // individual repository's owner
 | 
				
			||||||
        issue.userName ::
 | 
					        issue.userName ::
 | 
				
			||||||
@@ -27,7 +28,7 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    .distinct
 | 
					    .distinct
 | 
				
			||||||
    .withFilter ( _ != context.loginAccount.get.userName )	// the operation in person is excluded
 | 
					    .withFilter ( _ != context.loginAccount.get.userName )	// the operation in person is excluded
 | 
				
			||||||
    .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
 | 
					    .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,9 +69,8 @@ class Mailer(private val smtp: Smtp) extends Notifier {
 | 
				
			|||||||
      (msg: String => String)(implicit context: Context) = {
 | 
					      (msg: String => String)(implicit context: Context) = {
 | 
				
			||||||
    val database = Database(context.request.getServletContext)
 | 
					    val database = Database(context.request.getServletContext)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val f = future {
 | 
					    val f = Future {
 | 
				
			||||||
      // TODO Can we use the Database Session in other than Transaction Filter?
 | 
					      database withSession { implicit session =>
 | 
				
			||||||
      database withSession {
 | 
					 | 
				
			||||||
        getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
 | 
					        getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
 | 
				
			||||||
          defining(
 | 
					          defining(
 | 
				
			||||||
              s"[${r.name}] ${issue.title} (#${issueId})" ->
 | 
					              s"[${r.name}] ${issue.title} (#${issueId})" ->
 | 
				
			||||||
@@ -91,6 +91,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
 | 
				
			|||||||
                .foreach { case (address, name) =>
 | 
					                .foreach { case (address, name) =>
 | 
				
			||||||
                  email.setFrom(address, name)
 | 
					                  email.setFrom(address, name)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					              email.setCharset("UTF-8")
 | 
				
			||||||
              email.setSubject(subject)
 | 
					              email.setSubject(subject)
 | 
				
			||||||
              email.setHtmlMsg(msg)
 | 
					              email.setHtmlMsg(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ package util
 | 
				
			|||||||
import java.net.{URLDecoder, URLEncoder}
 | 
					import java.net.{URLDecoder, URLEncoder}
 | 
				
			||||||
import org.mozilla.universalchardet.UniversalDetector
 | 
					import org.mozilla.universalchardet.UniversalDetector
 | 
				
			||||||
import util.ControlUtil._
 | 
					import util.ControlUtil._
 | 
				
			||||||
 | 
					import org.apache.commons.io.input.BOMInputStream
 | 
				
			||||||
 | 
					import org.apache.commons.io.IOUtils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object StringUtil {
 | 
					object StringUtil {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,7 +29,12 @@ object StringUtil {
 | 
				
			|||||||
  def escapeHtml(value: String): String =
 | 
					  def escapeHtml(value: String): String =
 | 
				
			||||||
    value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
 | 
					    value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
 | 
					  /**
 | 
				
			||||||
 | 
					   * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]].
 | 
				
			||||||
 | 
					   * And if given bytes contains UTF-8 BOM, it's removed from returned string.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def convertFromByteArray(content: Array[Byte]): String =
 | 
				
			||||||
 | 
					    IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def detectEncoding(content: Array[Byte]): String =
 | 
					  def detectEncoding(content: Array[Byte]): String =
 | 
				
			||||||
    defining(new UniversalDetector(null)){ detector =>
 | 
					    defining(new UniversalDetector(null)){ detector =>
 | 
				
			||||||
@@ -38,4 +45,39 @@ object StringUtil {
 | 
				
			|||||||
        case e    => e
 | 
					        case e    => e
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Converts line separator in the given content.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param content the content
 | 
				
			||||||
 | 
					   * @param lineSeparator "LF" or "CRLF"
 | 
				
			||||||
 | 
					   * @return the converted content
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def convertLineSeparator(content: String, lineSeparator: String): String = {
 | 
				
			||||||
 | 
					    val lf = content.replace("\r\n", "\n").replace("\r", "\n")
 | 
				
			||||||
 | 
					    if(lineSeparator == "CRLF"){
 | 
				
			||||||
 | 
					      lf.replace("\n", "\r\n")
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      lf
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Extract issue id like ```#issueId``` from the given message.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   *@param message the message which may contains issue id
 | 
				
			||||||
 | 
					   * @return the iterator of issue id
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def extractIssueId(message: String): Iterator[String] =
 | 
				
			||||||
 | 
					    "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Extract close issue id like ```close #issueId ``` from the given message.
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param message the message which may contains close command
 | 
				
			||||||
 | 
					   * @return the iterator of issue id
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  def extractCloseId(message: String): Iterator[String] =
 | 
				
			||||||
 | 
					    "(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
package util
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import jp.sf.amateras.scalatra.forms._
 | 
					import jp.sf.amateras.scalatra.forms._
 | 
				
			||||||
 | 
					import org.scalatra.i18n.Messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait Validations {
 | 
					trait Validations {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,8 +9,8 @@ trait Validations {
 | 
				
			|||||||
   * Constraint for the identifier such as user name, repository name or page name.
 | 
					   * Constraint for the identifier such as user name, repository name or page name.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  def identifier: Constraint = new Constraint(){
 | 
					  def identifier: Constraint = new Constraint(){
 | 
				
			||||||
    override def validate(name: String, value: String): Option[String] =
 | 
					    override def validate(name: String, value: String, messages: Messages): Option[String] =
 | 
				
			||||||
      if(!value.matches("^[a-zA-Z0-9\\-_.]+$")){
 | 
					      if(!value.matches("[a-zA-Z0-9\\-_.]+")){
 | 
				
			||||||
        Some(s"${name} contains invalid character.")
 | 
					        Some(s"${name} contains invalid character.")
 | 
				
			||||||
      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
					      } else if(value.startsWith("_") || value.startsWith("-")){
 | 
				
			||||||
        Some(s"${name} starts with invalid character.")
 | 
					        Some(s"${name} starts with invalid character.")
 | 
				
			||||||
@@ -25,7 +26,7 @@ trait Validations {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  def date(constraints: Constraint*): SingleValueType[java.util.Date] =
 | 
					  def date(constraints: Constraint*): SingleValueType[java.util.Date] =
 | 
				
			||||||
    new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
 | 
					    new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
 | 
				
			||||||
      def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
 | 
					      def convert(value: String, messages: Messages): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user