diff --git a/README.md b/README.md
index 1e8d132..6883b24 100644
--- a/README.md
+++ b/README.md
@@ -6,20 +6,25 @@
The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http access only)
-- Repository viewer (some advanced features are not implemented)
+- Repository viewer (some advanced features such as online file editing are not implemented)
+- Repository search (Code and Issues)
- Wiki
- Issues
+- Fork / Pull request
+- Mail notification
- Activity timeline
- User management (for Administrators)
+- Group (like Organization in Github)
+- LDAP integration
+- Gravatar support
Following features are not implemented, but we will make them in the future release!
-- Fork and pull request
-- Search
+- File editing in repository viewer
+- Comment for the changeset
- Network graph
- Statics
- Watch / Star
-- Team management (like Organization in Github)
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -32,29 +37,64 @@
The default administrator account is **root** and password is **root**.
-To upgrade GitBucket, only replace gitbucket.war.
+(Since 1.6) or you can start GitBucket by ```java -jar gitbucket.war``` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
+
+- --port=[NUMBER]
+- --prefix=[CONTEXTPATH]
+
+To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
+
+For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
Release Notes
--------
-### 1.3 - xx Jul 2013
-- Batch updating for issues.
-- Display assigned user on issue list.
-- User icon and Gravatar support.
-- Convert @xxxx to link to the account page.
-- Add copy to clipboard button for git clone URL.
-- Allows multi-byte characters as wiki page name.
-- Allows to create the empty repository.
-- Fixed some bugs.
+### 1.6 - 1 Oct 2013
+- Web hook
+- Performance improvement for pull request
+- Executable war file
+- Specify suitable Content-Type for downloaded files in the repository viewer
+- Fix some bugs
+
+### 1.5 - 4 Sep 2013
+- Fork and pull request
+- LDAP authentication
+- Mail notification
+- Add an option to turn off the gravatar support
+- Add the branch tab in the repository viewer
+- Encoding auto detection for the file content in the repository viewer
+- Add favicon, header logo and icons for the timeline
+- Specify data directory via environment variable GITBUCKET_HOME
+- Fix some bugs
+
+### 1.4 - 31 Jul 2013
+- Group management
+- Repository search for code and issues
+- Display user related issues on the dashboard
+- Display participants avatar of issues on the issue page
+- Performance improvement for repository viewer
+- Alert by milestone due date
+- H2 database administration console
+- Fix some bugs
+
+### 1.3 - 18 Jul 2013
+- Batch updating for issues
+- Display assigned user on issue list
+- User icon and Gravatar support
+- Convert @xxxx to link to the account page
+- Add copy to clipboard button for git clone URL
+- Allow multi-byte characters as wiki page name
+- Allow to create the empty repository
+- Fix some bugs
### 1.2 - 09 Jul 2013
-- Added activity timeline.
-- Bugfix for Git 1.8.1.5 or later.
-- Allows multi-byte characters as label.
-- Fixed some bugs.
+- Add activity timeline
+- Bugfix for Git 1.8.1.5 or later
+- Allow multi-byte characters as label
+- Fix some bugs
### 1.1 - 05 Jul 2013
-- Fixed some bugs.
-- Upgrade to JGit 3.0.
+- Fix some bugs
+- Upgrade to JGit 3.0
### 1.0 - 04 Jul 2013
-- This is a first public release.
+- This is a first public release
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..187359e
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/embed-jetty/javax.servlet-3.0.0.v201112011016.jar b/embed-jetty/javax.servlet-3.0.0.v201112011016.jar
new file mode 100644
index 0000000..b135409
--- /dev/null
+++ b/embed-jetty/javax.servlet-3.0.0.v201112011016.jar
Binary files differ
diff --git a/embed-jetty/jetty-continuation-8.1.8.v20121106.jar b/embed-jetty/jetty-continuation-8.1.8.v20121106.jar
new file mode 100644
index 0000000..1f3f59c
--- /dev/null
+++ b/embed-jetty/jetty-continuation-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-http-8.1.8.v20121106.jar b/embed-jetty/jetty-http-8.1.8.v20121106.jar
new file mode 100644
index 0000000..80a2ba7
--- /dev/null
+++ b/embed-jetty/jetty-http-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-io-8.1.8.v20121106.jar b/embed-jetty/jetty-io-8.1.8.v20121106.jar
new file mode 100644
index 0000000..21d1d67
--- /dev/null
+++ b/embed-jetty/jetty-io-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-security-8.1.8.v20121106.jar b/embed-jetty/jetty-security-8.1.8.v20121106.jar
new file mode 100644
index 0000000..aac3f19
--- /dev/null
+++ b/embed-jetty/jetty-security-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-server-8.1.8.v20121106.jar b/embed-jetty/jetty-server-8.1.8.v20121106.jar
new file mode 100644
index 0000000..b21842e
--- /dev/null
+++ b/embed-jetty/jetty-server-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-servlet-8.1.8.v20121106.jar b/embed-jetty/jetty-servlet-8.1.8.v20121106.jar
new file mode 100644
index 0000000..df9583f
--- /dev/null
+++ b/embed-jetty/jetty-servlet-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-util-8.1.8.v20121106.jar b/embed-jetty/jetty-util-8.1.8.v20121106.jar
new file mode 100644
index 0000000..18c2270
--- /dev/null
+++ b/embed-jetty/jetty-util-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-webapp-8.1.8.v20121106.jar b/embed-jetty/jetty-webapp-8.1.8.v20121106.jar
new file mode 100644
index 0000000..23d18ab
--- /dev/null
+++ b/embed-jetty/jetty-webapp-8.1.8.v20121106.jar
Binary files differ
diff --git a/embed-jetty/jetty-xml-8.1.8.v20121106.jar b/embed-jetty/jetty-xml-8.1.8.v20121106.jar
new file mode 100644
index 0000000..f8daf47
--- /dev/null
+++ b/embed-jetty/jetty-xml-8.1.8.v20121106.jar
Binary files differ
diff --git a/etc/gitbucket.erd b/etc/gitbucket.erd
new file mode 100644
index 0000000..74f5fb4
--- /dev/null
+++ b/etc/gitbucket.erd
@@ -0,0 +1,1745 @@
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 33
+ 18
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 723
+ 138
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 1182
+ 339
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 1301
+ 836
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 684
+ 858
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 293
+ 478
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ ISSUE_FK_1
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 875
+ 677
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ MILESTONE_FK_1
+
+
+
+
+
+
+
+
+ MILESTONE
+ Milestone
+
+
+
+ USER_NAME
+ User Name
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ MILESTONE_ID
+ Milestone ID
+
+ INT
+ 整数
+ false
+ 4
+
+ 10
+ true
+ true
+
+ true
+
+
+
+ TITLE
+ Title
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 100
+ true
+ false
+
+ false
+
+
+
+ DESCRIPTION
+ Description
+
+ TEXT
+ 文字列
+ true
+ 2005
+
+
+ false
+ false
+
+ false
+
+
+
+ DUE_DATE
+ Due Date
+
+ TIMESTAMP
+ 日時
+ false
+ 93
+
+ 10
+ false
+ false
+
+ false
+
+
+
+ CLOSED_DATE
+ Closed Date
+
+ 10
+ false
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+ ISSUE_FK_2
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ ISSUE_FK_2
+
+
+
+ USER_NAME
+ User Name
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ OPENED_USER_NAME
+ Opened User Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ ISSUE_FK_2
+
+
+
+
+ ASSIGNED_USER_NAME
+ Assinged User Name
+
+ 100
+ false
+ false
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 18
+ 776
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ ISSUE_COMMENT_FK_2
+
+
+
+
+ COMMENTED_USER_NAME
+ Commented User Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+
+
+
+
+
+
+ ISSUE_COMMENT
+ Issue Comment
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ ISSUE_ID
+ Issue ID
+
+ 10
+ true
+ true
+
+ false
+
+
+
+ COMMENT_ID
+ Comment ID
+
+ 10
+ true
+ true
+
+ true
+
+
+
+ ACTION
+ Action
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 20
+ true
+ false
+ Expand to VARCHAR(20) from VARCHAR(10) in 1.3
+ false
+
+
+
+
+ CONTENT
+ Content
+
+ TEXT
+ 文字列
+ true
+ 2005
+
+
+ true
+ false
+
+ false
+
+
+
+ REGISTERED_DATE
+ Registered Date
+
+ TIMESTAMP
+ 日時
+ false
+ 93
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ UPDATED_DATE
+ Updated Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ ISSUE_COMMENT_FK_1
+
+
+
+
+
+
+ ISSUE
+ Issue
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ ISSUE_ID
+ Issue ID
+
+ 10
+ true
+ true
+
+ false
+
+
+
+
+ MILESTONE_ID
+ Milestone ID
+
+ 10
+ false
+ false
+
+ false
+
+
+
+
+ TITLE
+ Title
+
+
+ true
+ false
+
+ false
+
+
+
+ CONTENT
+ Content
+
+
+ true
+ false
+
+ false
+
+
+
+ REGISTERED_DATE
+ Registered Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ UPDATED_DATE
+ Updated Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+ ISSUE_LABEL_FK_2
+
+
+
+
+
+
+ ISSUE_LABEL
+ Issue Label
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ ISSUE_ID
+ Issue ID
+
+ 10
+ true
+ true
+
+ false
+
+
+
+ LABEL_ID
+ Label ID
+
+ 10
+ true
+ true
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ ISSUE_LABEL_FK_1
+
+
+
+
+
+ LABEL
+ Label
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ false
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ false
+ true
+
+ false
+
+
+
+ LABEL_ID
+ Label ID
+
+ INT
+ 整数
+ false
+ 4
+
+ 10
+ true
+ true
+
+ true
+
+
+
+ LABEL_NAME
+ Label Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+ COLOR
+ Color
+
+ CHAR
+ 文字
+ true
+ 1
+
+ 6
+ true
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ LABEL_FK_1
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 481
+ 361
+
+
+
+
+
+
+
+ ISSUE_ID
+ Issue ID
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ false
+ false
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ false
+ false
+
+ false
+
+
+
+ ISSUE_ID
+ Issue ID
+
+ 10
+ true
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ ISSUE_ID_FK_1
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 1199
+ 25
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ ACTIVITY_FK_2
+
+
+
+
+ ACTIVITY_USER_NAME
+ Activity User Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+
+
+
+
+
+
+ ACTIVITY
+ Activity
+ Since 1.2
+
+
+ ACTIVITY_ID
+ Activity ID
+
+ INT
+ 整数
+ false
+ 4
+
+ 10
+ true
+ true
+
+ true
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ false
+
+ false
+
+
+
+
+ ACTIVITY_TYPE
+ Activity Type
+
+ 100
+ true
+ false
+
+ false
+
+
+
+ MESSAGE
+ Message
+
+
+ true
+ false
+
+ false
+
+
+
+ ADDITIONAL_INFO
+ Additional Information
+
+
+ false
+ false
+
+ false
+
+
+
+ ACTIVITY_DATE
+ Activity Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ ACTIVITY_FK_1
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 1451
+ 577
+
+
+
+
+
+
+
+ COMMIT_LOG
+ Commit Log
+ Since 1.2
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ COMMIT_ID
+ Commit ID
+
+ 40
+ true
+ true
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ COMMIT_LOG_FK_1
+
+
+
+
+
+ REPOSITORY
+ Repository
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ REPOSITORY_TYPE
+ Repository Type
+
+ 10
+ true
+ false
+ 0:Public 1:Private
+ false
+ 0
+
+
+ DESCRIPTION
+ Description
+
+ TEXT
+ 文字列
+ true
+ 2005
+
+
+ false
+ false
+
+ false
+
+
+
+ DEFAULT_BRANCH
+ Default Branch
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 100
+ false
+ false
+
+ false
+
+
+
+ REGISTERED_DATE
+ Registered Date
+
+ TIMESTAMP
+ 日時
+ false
+ 93
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ UPDATED_DATE
+ Updated Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ LAST_ACTIVITY_DATE
+ Last Activity Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+
+
+ IDX_PROJECT_1
+
+ UNIQUE
+
+
+ PROJECT_NAME
+ USER_ID
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+ PROJECT_ACCOUNT_FK_1
+
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+ COLLABORATORS
+ Collaborators
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+
+ COLLABORATOR_NAME
+ Collaborator Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ PROJECT_ACCOUNT_FK_2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 432
+ 240
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
+ GROUP_MEMBER_FK_2
+
+
+
+
+
+
+ GROUP_MEMBER
+ Group Member
+ Since 1.4
+
+
+ GROUP_NAME
+ Group Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+ USER_NAME
+ User Name
+
+ 100
+ true
+ true
+
+ false
+
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+ GROUP_MEMBER_FK_1
+
+
+
+
+
+
+
+
+
+
+
+ ACCOUNT
+ Account
+
+
+
+
+ MAIL_ADDRESS
+ Mail Address
+
+ VARCHAR
+ 文字列
+ true
+ 12
+
+ 100
+ true
+ false
+
+ false
+
+
+
+ PASSWORD
+ Password
+
+ 40
+ true
+ false
+
+ false
+
+
+
+ ADMINISTRATOR
+ Administrator
+
+ BOOLEAN
+ 真偽値
+ false
+ 16
+
+ 10
+ true
+ false
+
+ false
+ 0
+
+
+ URL
+ URL
+
+ 200
+ false
+ false
+
+ false
+
+
+
+ REGISTERED_DATE
+ Registered Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ UPDATED_DATE
+ Updated Date
+
+ 10
+ true
+ false
+
+ false
+
+
+
+ LAST_LOGIN_DATE
+ Last Login Date
+
+ 10
+ false
+ false
+
+ false
+
+
+
+ IMAGE
+ Image
+
+ 100
+ false
+ false
+ Since 1.3
+ false
+
+
+
+ GROUP_ACCOUNT
+ Group Account
+
+ BOOLEAN
+ 真偽値
+ false
+ 16
+
+ 10
+ true
+ false
+ Since 1.4
+ false
+ FALSE
+
+
+
+
+ IDX_ACCOUNT_1
+
+ UNIQUE
+
+
+ MAIL_ADDRESS
+
+
+
+
+ 255
+ 255
+ 206
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ -1
+ -1
+ 410
+ 860
+
+
+
+
+
+ ISSUE_OUTLINE_VIEW
+ Issue Outline View
+ Since 1.4
+
+
+ USER_NAME
+ User Name
+
+ 100
+ false
+ false
+
+ false
+
+
+
+ REPOSITORY_NAME
+ Repository Name
+
+ 100
+ false
+ false
+
+ false
+
+
+
+ ISSUE_ID
+ Issue ID
+
+ INT
+ 整数
+ false
+ 4
+
+ 10
+ false
+ false
+
+ false
+
+
+
+ COMMENT_COUNT
+ Comment Count
+
+ 10
+ false
+ false
+
+ false
+
+
+
+
+
+ 210
+ 232
+ 249
+
+
+
+
+
+ H2
+ false
+
+ sun.jdbc.odbc.JdbcOdbc
+
+
+
+
+
+ false
+
+
\ No newline at end of file
diff --git a/etc/icons.svg b/etc/icons.svg
new file mode 100644
index 0000000..6943304
--- /dev/null
+++ b/etc/icons.svg
@@ -0,0 +1,751 @@
+
+
+
+
diff --git a/gitbucket.erd b/gitbucket.erd
deleted file mode 100644
index 7b63794..0000000
--- a/gitbucket.erd
+++ /dev/null
@@ -1,1200 +0,0 @@
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 37
- 36
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 751
- 47
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 882
- 239
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 940
- 615
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 420
- 758
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 307
- 356
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- ISSUE_FK_1
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 641
- 569
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- MILESTONE_FK_1
-
-
-
-
-
-
-
-
- MILESTONE
- Milestone
-
-
-
- USER_NAME
- User Name
-
- VARCHAR
- 文字列
- true
- 12
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
- MILESTONE_ID
- Milestone ID
-
- INT
- 整数
- false
- 4
-
- 10
- true
- true
-
- true
-
-
-
- MILESTONE_NAME
- Milestone Name
-
- 100
- true
- false
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
- ISSUE_FK_2
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- ISSUE_FK_2
-
-
-
- USER_NAME
- User Name
-
- VARCHAR
- 文字列
- true
- 12
-
- 100
- true
- true
-
- false
-
-
-
- OPENED_USER_NAME
- Opened User Name
-
- 100
- true
- false
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 26
- 660
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
- ISSUE_COMMENT_FK_2
-
-
-
-
- COMMENTED_USER_NAME
- Commented User Name
-
- 100
- true
- false
-
- false
-
-
-
-
-
-
-
-
-
- ISSUE_COMMENT
- Issue Comment
-
-
-
- USER_NAME
- User Name
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
- ISSUE_ID
- Issue ID
-
- 10
- true
- true
-
- false
-
-
-
- COMMENT_ID
- Comment ID
-
- 10
- true
- true
-
- true
-
-
-
-
- CONTENT
- Content
-
- TEXT
- 文字列
- true
- 2005
-
-
- true
- false
-
- false
-
-
-
- REGISTERED_DATE
- Registered Date
-
- TIMESTAMP
- 日時
- false
- 93
-
- 10
- true
- false
-
- false
-
-
-
- UPDATED_DATE
- Updated Date
-
- 10
- true
- false
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
-
- ISSUE_COMMENT_FK_1
-
-
-
-
-
-
- ISSUE
- Issue
-
-
-
- USER_NAME
- User Name
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
- ISSUE_ID
- Issue ID
-
- 10
- true
- true
-
- false
-
-
-
-
- MILESTONE_ID
- Milestone ID
-
- 10
- false
- false
-
- false
-
-
-
- TITLE
- Title
-
-
- true
- false
-
- false
-
-
-
- CONTENT
- Content
-
-
- true
- false
-
- false
-
-
-
- REGISTERED_DATE
- Registered Date
-
- 10
- true
- false
-
- false
-
-
-
- UPDATED_DATE
- Updated Date
-
- 10
- true
- false
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
- ISSUE_LABEL_FK_2
-
-
-
-
-
-
- ISSUE_LABEL
- Issue Label
-
-
-
- USER_NAME
- User Name
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
- ISSUE_ID
- Issue ID
-
- 10
- true
- true
-
- false
-
-
-
- LABEL_ID
- Label ID
-
- 10
- true
- true
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
-
- ISSUE_LABEL_FK_1
-
-
-
-
-
- LABEL
- Label
-
-
-
- USER_NAME
- User Name
-
- 100
- false
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- false
- true
-
- false
-
-
-
- LABEL_ID
- Label ID
-
- INT
- 整数
- false
- 4
-
- 10
- true
- true
-
- true
-
-
-
- LABEL_NAME
- Label Name
-
- 100
- true
- false
-
- false
-
-
-
- COLOR
- Color
-
- CHAR
- 文字
- true
- 1
-
- 6
- true
- false
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
-
- LABEL_FK_1
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
- -1
- -1
- 388
- 166
-
-
-
-
-
-
-
- ISSUE_ID
- Issue ID
-
-
-
- USER_NAME
- User Name
-
- 100
- false
- false
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- false
- false
-
- false
-
-
-
- ISSUE_ID
- Issue ID
-
- 10
- true
- false
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
-
- ISSUE_ID_FK_1
-
-
-
-
-
-
-
- REPOSITORY
- Repository
-
-
-
- USER_NAME
- User Name
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
- REPOSITORY_TYPE
- Repository Type
-
- 10
- true
- false
- 0:Public 1:Private
- false
- 0
-
-
- DESCRIPTION
- Description
-
- TEXT
- 文字列
- true
- 2005
-
-
- false
- false
-
- false
-
-
-
- DEFAULT_BRANCH
- Default Branch
-
- VARCHAR
- 文字列
- true
- 12
-
- 100
- false
- false
-
- false
-
-
-
- REGISTERED_DATE
- Registered Date
-
- TIMESTAMP
- 日時
- false
- 93
-
- 10
- true
- false
-
- false
-
-
-
- UPDATED_DATE
- Updated Date
-
- 10
- true
- false
-
- false
-
-
-
- LAST_ACTIVITY_DATE
- Last Activity Date
-
- 10
- true
- false
-
- false
-
-
-
-
-
- IDX_PROJECT_1
-
- UNIQUE
-
-
- PROJECT_NAME
- USER_ID
-
-
-
-
- 255
- 255
- 206
-
-
-
- PROJECT_ACCOUNT_FK_1
-
-
-
-
- REPOSITORY_NAME
- Repository Name
-
- 100
- true
- true
-
- false
-
-
-
-
-
-
-
-
-
-
- COLLABORATORS
- Collaborators
-
-
-
- USER_NAME
- User Name
-
- 100
- true
- true
-
- false
-
-
-
-
- COLLABORATOR_NAME
- Collaborator Name
-
- 100
- true
- true
-
- false
-
-
-
-
-
- 255
- 255
- 206
-
-
-
-
- PROJECT_ACCOUNT_FK_2
-
-
-
-
-
-
-
-
-
-
-
-
- ACCOUNT
- Account
-
-
-
-
- MAIL_ADDRESS
- Mail Address
-
- VARCHAR
- 文字列
- true
- 12
-
- 100
- true
- false
-
- false
-
-
-
- PASSWORD
- Password
-
- 20
- true
- false
-
- false
-
-
-
- USER_TYPE
- User Type
-
- INT
- 整数
- false
- 4
-
- 10
- true
- false
- 0:Normal 1:Administrator
- false
- 0
-
-
- URL
- URL
-
- 200
- false
- false
-
- false
-
-
-
- REGISTERED_DATE
- Registered Date
-
- 10
- true
- false
-
- false
-
-
-
- UPDATED_DATE
- Updated Date
-
- 10
- true
- false
-
- false
-
-
-
- LAST_LOGIN_DATE
- Last Login Date
-
- 10
- false
- false
-
- false
-
-
-
-
-
- IDX_ACCOUNT_1
-
- UNIQUE
-
-
- MAIL_ADDRESS
-
-
-
-
- 255
- 255
- 206
-
-
-
-
-
-
-
-
-
-
-
-
-
- H2
- false
-
- sun.jdbc.odbc.JdbcOdbc
-
-
-
-
-
- false
-
-
\ No newline at end of file
diff --git a/lib/scalatra-forms_2.10-0.0.1.jar b/lib/scalatra-forms_2.10-0.0.1.jar
deleted file mode 100644
index baea441..0000000
--- a/lib/scalatra-forms_2.10-0.0.1.jar
+++ /dev/null
Binary files differ
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..db255c2
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.12.3
\ No newline at end of file
diff --git a/project/build.scala b/project/build.scala
index 774c0dd..61db306 100644
--- a/project/build.scala
+++ b/project/build.scala
@@ -2,6 +2,7 @@
import Keys._
import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
+import sbt.ScalaVersion
import twirl.sbt.TwirlPlugin._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
@@ -9,8 +10,8 @@
val Organization = "jp.sf.amateras"
val Name = "gitbucket"
val Version = "0.0.1"
- val ScalaVersion = "2.10.1"
- val ScalatraVersion = "2.2.0"
+ val ScalaVersion = "2.10.3"
+ val ScalatraVersion = "2.2.1"
lazy val project = Project (
"gitbucket",
@@ -20,24 +21,35 @@
name := Name,
version := Version,
scalaVersion := ScalaVersion,
- resolvers += Classpaths.typesafeReleases,
+ resolvers ++= Seq(
+ Classpaths.typesafeReleases,
+ "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
+ ),
+ scalacOptions := Seq("-deprecation"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
- "org.apache.commons" % "commons-io" % "1.3.2",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
- "org.json4s" %% "json4s-jackson" % "3.2.4",
+ "org.json4s" %% "json4s-jackson" % "3.2.5",
+ "jp.sf.amateras" %% "scalatra-forms" % "0.0.2",
"commons-io" % "commons-io" % "2.4",
- "org.pegdown" % "pegdown" % "1.3.0",
+ "org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
+ "org.apache.commons" % "commons-email" % "1.3.1",
+ "org.apache.httpcomponents" % "httpclient" % "4.3",
"com.typesafe.slick" %% "slick" % "1.0.1",
- "com.h2database" % "h2" % "1.3.171",
- "ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
- "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container",
- "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar"))
+ "com.novell.ldap" % "jldap" % "2009-10-07",
+ "com.h2database" % "h2" % "1.3.173",
+ "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
+ "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
+ "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts (Artifact("javax.servlet", "jar", "jar")),
+ "junit" % "junit" % "4.11" % "test"
),
- EclipseKeys.withSource := true
+ EclipseKeys.withSource := true,
+ javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
+ testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
+ packageOptions += Package.MainClass("JettyLauncher")
) ++ seq(Twirl.settings: _*)
)
-}
\ No newline at end of file
+}
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 3249832..15ac806 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,7 +1,9 @@
-addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2")
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.2.0")
-addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0")
+addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.1")
-addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.2.0")
+addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.0")
addSbtPlugin("io.spray" % "sbt-twirl" % "0.6.1")
+
+addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.2")
diff --git a/sbt.sh b/sbt.sh
new file mode 100755
index 0000000..dd2a62a
--- /dev/null
+++ b/sbt.sh
@@ -0,0 +1 @@
+java -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.12.3.jar "$@"
diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java
new file mode 100644
index 0000000..356da21
--- /dev/null
+++ b/src/main/java/JettyLauncher.java
@@ -0,0 +1,53 @@
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.webapp.WebAppContext;
+
+import java.net.URL;
+import java.security.ProtectionDomain;
+
+public class JettyLauncher {
+ public static void main(String[] args) throws Exception {
+ String host = null;
+ int port = 8080;
+ String contextPath = "/";
+
+ for(String arg: args){
+ if(arg.startsWith("--") && arg.contains("=")){
+ String[] dim = arg.split("=");
+ if(dim.length >= 2){
+ if(dim[0].equals("--host")){
+ host = dim[1];
+ } else if(dim[0].equals("--port")){
+ port = Integer.parseInt(dim[1]);
+ } else if(dim[0].equals("--prefix")){
+ contextPath = dim[1];
+ }
+ }
+ }
+ }
+
+ Server server = new Server();
+
+ SelectChannelConnector connector = new SelectChannelConnector();
+ if(host != null){
+ connector.setHost(host);
+ }
+ connector.setMaxIdleTime(1000 * 60 * 60);
+ connector.setSoLingerTime(-1);
+ connector.setPort(port);
+ server.addConnector(connector);
+
+ WebAppContext context = new WebAppContext();
+ ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
+ URL location = domain.getCodeSource().getLocation();
+
+ context.setContextPath(contextPath);
+ context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml");
+ context.setServer(server);
+ context.setWar(location.toExternalForm());
+
+ server.setHandler(context);
+ server.start();
+ server.join();
+ }
+}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 1fdfbec..fccd344 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -1,4 +1,17 @@
-
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/noimage.png b/src/main/resources/noimage.png
index 39d1ab4..b338e16 100644
--- a/src/main/resources/noimage.png
+++ b/src/main/resources/noimage.png
Binary files differ
diff --git a/src/main/resources/update/1_4.sql b/src/main/resources/update/1_4.sql
new file mode 100644
index 0000000..2d3c492
--- /dev/null
+++ b/src/main/resources/update/1_4.sql
@@ -0,0 +1,24 @@
+CREATE TABLE GROUP_MEMBER(
+ GROUP_NAME VARCHAR(100) NOT NULL,
+ USER_NAME VARCHAR(100) NOT NULL
+);
+
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
+ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
+
+ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
+
+CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
+ SELECT
+ A.USER_NAME,
+ A.REPOSITORY_NAME,
+ A.ISSUE_ID,
+ NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
+ FROM ISSUE A
+ LEFT OUTER JOIN (
+ SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
+ WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
+ GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
+ ) B
+ ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);
diff --git a/src/main/resources/update/1_5.sql b/src/main/resources/update/1_5.sql
new file mode 100644
index 0000000..03fc1bf
--- /dev/null
+++ b/src/main/resources/update/1_5.sql
@@ -0,0 +1,21 @@
+ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
+ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
+ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
+ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
+
+CREATE TABLE PULL_REQUEST(
+ USER_NAME VARCHAR(100) NOT NULL,
+ REPOSITORY_NAME VARCHAR(100) NOT NULL,
+ ISSUE_ID INT NOT NULL,
+ BRANCH VARCHAR(100) NOT NULL,
+ REQUEST_USER_NAME VARCHAR(100) NOT NULL,
+ REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
+ REQUEST_BRANCH VARCHAR(100) NOT NULL,
+ COMMIT_ID_FROM VARCHAR(40) NOT NULL,
+ COMMIT_ID_TO VARCHAR(40) NOT NULL
+);
+
+ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
+ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
+
+ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/src/main/resources/update/1_6.sql b/src/main/resources/update/1_6.sql
new file mode 100644
index 0000000..43eb92d
--- /dev/null
+++ b/src/main/resources/update/1_6.sql
@@ -0,0 +1,8 @@
+CREATE TABLE WEB_HOOK (
+ USER_NAME VARCHAR(100) NOT NULL,
+ REPOSITORY_NAME VARCHAR(100) NOT NULL,
+ URL VARCHAR(200) NOT NULL
+);
+
+ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
+ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
diff --git a/src/main/resources/update/1_7.sql b/src/main/resources/update/1_7.sql
new file mode 100644
index 0000000..9005ff9
--- /dev/null
+++ b/src/main/resources/update/1_7.sql
@@ -0,0 +1,5 @@
+ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
+
+UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
+
+ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index f84c99c..874bbbd 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -5,8 +5,10 @@
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
context.mount(new IndexController, "/")
+ context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new SignInController, "/*")
+ context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new CreateRepositoryController, "/*")
@@ -16,6 +18,7 @@
context.mount(new LabelsController, "/*")
context.mount(new MilestonesController, "/*")
context.mount(new IssuesController, "/*")
+ context.mount(new PullRequestsController, "/*")
context.mount(new RepositorySettingsController, "/*")
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala
index 68b2d53..6691bcc 100644
--- a/src/main/scala/app/AccountController.scala
+++ b/src/main/scala/app/AccountController.scala
@@ -1,11 +1,10 @@
package app
import service._
-import util.{FileUtil, FileUploadUtil, OneselfAuthenticator}
+import util.{FileUtil, OneselfAuthenticator}
import util.StringUtil._
import util.Directory._
import jp.sf.amateras.scalatra.forms._
-import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
class AccountController extends AccountControllerBase
@@ -16,15 +15,16 @@
self: SystemSettingsService with AccountService with RepositoryService with ActivityService
with OneselfAuthenticator =>
- case class AccountNewForm(userName: String, password: String,mailAddress: String,
+ case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
- case class AccountEditForm(password: Option[String], mailAddress: String,
+ case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String], clearImage: Boolean)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
+ "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
@@ -32,6 +32,7 @@
val editForm = mapping(
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
+ "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
@@ -43,12 +44,23 @@
*/
get("/:userName") {
val userName = params("userName")
- getAccountByUserName(userName).map { x =>
+ getAccountByUserName(userName).map { account =>
params.getOrElse("tab", "repositories") match {
// Public Activity
- case "activity" => account.html.activity(x, getActivitiesByUser(userName, true))
+ case "activity" =>
+ _root_.account.html.activity(account,
+ if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
+ getActivitiesByUser(userName, true))
+
+ // Members
+ case "members" if(account.isGroupAccount) =>
+ _root_.account.html.members(account, getGroupMembers(account.userName))
+
// Repositories
- case _ => account.html.repositories(x, getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
+ case _ =>
+ _root_.account.html.repositories(account,
+ if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
+ getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}
@@ -74,6 +86,7 @@
getAccountByUserName(userName).map { account =>
updateAccount(account.copy(
password = form.password.map(sha1).getOrElse(account.password),
+ fullName = form.fullName,
mailAddress = form.mailAddress,
url = form.url))
@@ -96,7 +109,7 @@
post("/register", newForm){ form =>
if(loadSystemSettings().allowAccountRegistration){
- createAccount(form.userName, sha1(form.password), form.mailAddress, false, form.url)
+ createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/signin")
} else NotFound
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index b338f36..a3406cc 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -1,7 +1,9 @@
package app
import _root_.util.Directory._
-import _root_.util.{FileUploadUtil, FileUtil, Validations}
+import _root_.util.Implicits._
+import _root_.util.ControlUtil._
+import _root_.util.{FileUtil, Validations, Keys}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -10,7 +12,9 @@
import model.Account
import scala.Some
import service.AccountService
-import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
+import java.text.SimpleDateFormat
+import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
/**
* Provides generic features for controller implementations.
@@ -20,73 +24,94 @@
implicit val jsonFormats = DefaultFormats
+ // Don't set content type via Accept header.
+ override def format(implicit request: HttpServletRequest) = ""
+
+ override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
+ val httpRequest = request.asInstanceOf[HttpServletRequest]
+ val httpResponse = response.asInstanceOf[HttpServletResponse]
+ val context = request.getServletContext.getContextPath
+ val path = httpRequest.getRequestURI.substring(context.length)
+
+ if(path.startsWith("/console/")){
+ val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
+ if(account == null){
+ // Redirect to login form
+ httpResponse.sendRedirect(context + "/signin?" + path)
+ } else if(account.isAdmin){
+ // H2 Console (administrators only)
+ chain.doFilter(request, response)
+ } else {
+ // Redirect to dashboard
+ httpResponse.sendRedirect(context + "/")
+ }
+ } else if(path.startsWith("/git/")){
+ // Git repository
+ chain.doFilter(request, response)
+ } else {
+ // Scalatra actions
+ super.doFilter(request, response, chain)
+ }
+ }
+
/**
* Returns the context object for the request.
*/
implicit def context: Context = Context(servletContext.getContextPath, LoginAccount, currentURL, request)
- private def currentURL: String = {
- val queryString = request.getQueryString
+ private def currentURL: String = defining(request.getQueryString){ queryString =>
request.getRequestURI + (if(queryString != null) "?" + queryString else "")
}
- private def LoginAccount: Option[Account] = {
- session.get("LOGIN_ACCOUNT") match {
- case Some(x: Account) => Some(x)
- case _ => None
- }
- }
+ private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
- def ajaxGet(path : String)(action : => Any) : Route = {
+ def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){
- request.setAttribute("AJAX", "true")
+ request.setAttribute(Keys.Request.Ajax, "true")
action
}
- }
- override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
+ override def ajaxGet[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxGet(path, form){ form =>
- request.setAttribute("AJAX", "true")
+ request.setAttribute(Keys.Request.Ajax, "true")
action(form)
}
- }
- def ajaxPost(path : String)(action : => Any) : Route = {
+ def ajaxPost(path : String)(action : => Any) : Route =
super.post(path){
- request.setAttribute("AJAX", "true")
+ request.setAttribute(Keys.Request.Ajax, "true")
action
}
- }
- override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route = {
+ override def ajaxPost[T](path : String, form : MappingValueType[T])(action : T => Any) : Route =
super.ajaxPost(path, form){ form =>
- request.setAttribute("AJAX", "true")
+ request.setAttribute(Keys.Request.Ajax, "true")
action(form)
}
- }
- protected def NotFound() = {
- if(request.getAttribute("AJAX") == null){
- org.scalatra.NotFound(html.error("Not Found"))
- } else {
+ protected def NotFound() =
+ if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound()
+ } else {
+ org.scalatra.NotFound(html.error("Not Found"))
}
- }
- protected def Unauthorized()(implicit context: app.Context) = {
- if(request.getAttribute("AJAX") == null){
+ protected def Unauthorized()(implicit context: app.Context) =
+ if(request.hasAttribute(Keys.Request.Ajax)){
+ org.scalatra.Unauthorized()
+ } else {
if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/"))
} else {
- org.scalatra.Unauthorized(redirect("/signin?" + currentURL))
+ if(request.getMethod.toUpperCase == "POST"){
+ org.scalatra.Unauthorized(redirect("/signin"))
+ } else {
+ org.scalatra.Unauthorized(redirect("/signin?redirect=" + currentURL))
+ }
}
- } else {
- org.scalatra.Unauthorized()
}
- }
- protected def baseUrl = {
- val url = request.getRequestURL.toString
+ protected def baseUrl = defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
}
@@ -97,28 +122,36 @@
*/
case class Context(path: String, loginAccount: Option[Account], currentUrl: String, request: HttpServletRequest){
+ def redirectUrl = if(request.getParameter("redirect") != null){
+ request.getParameter("redirect")
+ } else {
+ currentUrl
+ }
+
/**
* Get object from cache.
*
* If object has not been cached with the specified key then retrieves by given action.
* Cached object are available during a request.
*/
- def cache[A](key: String)(action: => A): A = {
- Option(request.getAttribute("cache." + key).asInstanceOf[A]).getOrElse {
- val newObject = action
- request.setAttribute("cache." + key, newObject)
- newObject
+ def cache[A](key: String)(action: => A): A =
+ defining(Keys.Request.Cache(key)){ cacheKey =>
+ Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse {
+ val newObject = action
+ request.setAttribute(cacheKey, newObject)
+ newObject
+ }
}
- }
}
/**
* Base trait for controllers which manages account information.
*/
-trait AccountManagementControllerBase extends ControllerBase { self: AccountService =>
+trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase {
+ self: AccountService =>
- protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = {
+ protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit =
if(clearImage){
getAccountByUserName(userName).flatMap(_.image).map { image =>
new java.io.File(getUserUploadDir(userName), image).delete()
@@ -126,26 +159,50 @@
}
} else {
fileId.map { fileId =>
- val filename = "avatar." + FileUtil.getExtension(FileUploadUtil.getUploadedFilename(fileId).get)
+ val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get)
FileUtils.moveFile(
- FileUploadUtil.getTemporaryFile(fileId),
+ getTemporaryFile(fileId),
new java.io.File(getUserUploadDir(userName), filename)
)
updateAvatarImage(userName, Some(filename))
}
}
- }
protected def uniqueUserName: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value).map { _ => "User already exists." }
}
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getAccountByMailAddress(value)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." }
}
+}
+
+/**
+ * Base trait for controllers which needs file uploading feature.
+ */
+trait FileUploadControllerBase {
+
+ def generateFileId: String =
+ new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
+
+ def TemporaryDir(implicit session: HttpSession): java.io.File =
+ new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
+
+ def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
+ new java.io.File(TemporaryDir, fileId)
+
+ // def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
+ // getTemporaryFile(fileId).delete()
+
+ def removeTemporaryFiles()(implicit session: HttpSession): Unit =
+ FileUtils.deleteDirectory(TemporaryDir)
+
+ def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] =
+ session.getAndRemove[String](Keys.Session.Upload(fileId))
+
}
\ No newline at end of file
diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala
index e10e5bb..a3c52d0 100644
--- a/src/main/scala/app/CreateRepositoryController.scala
+++ b/src/main/scala/app/CreateRepositoryController.scala
@@ -1,107 +1,193 @@
package app
import util.Directory._
-import util.{JGitUtil, UsersAuthenticator}
+import util.ControlUtil._
+import util._
import service._
import java.io.File
import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.lib._
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
+import org.eclipse.jgit.lib.PersonIdent
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
- with UsersAuthenticator
+ with UsersAuthenticator with ReadableUsersAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
- self: RepositoryService with WikiService with LabelsService with ActivityService
- with UsersAuthenticator =>
+ self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
+ with UsersAuthenticator with ReadableUsersAuthenticator =>
- case class RepositoryCreationForm(name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
+ case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
- val form = mapping(
+ case class ForkRepositoryForm(owner: String, name: String)
+
+ val newForm = mapping(
+ "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
)(RepositoryCreationForm.apply)
+ val forkForm = mapping(
+ "owner" -> trim(label("Repository owner", text(required))),
+ "name" -> trim(label("Repository name", text(required)))
+ )(ForkRepositoryForm.apply)
+
/**
* Show the new repository form.
*/
get("/new")(usersOnly {
- html.newrepo()
+ html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
})
/**
* Create new repository.
*/
- post("/new", form)(usersOnly { form =>
- val loginAccount = context.loginAccount.get
- val loginUserName = loginAccount.userName
+ post("/new", newForm)(usersOnly { form =>
+ LockUtil.lock(s"${form.owner}/${form.name}/create"){
+ if(getRepository(form.owner, form.name, baseUrl).isEmpty){
+ val ownerAccount = getAccountByUserName(form.owner).get
+ val loginAccount = context.loginAccount.get
+ val loginUserName = loginAccount.userName
- // Insert to the database at first
- createRepository(form.name, loginUserName, form.description, form.isPrivate)
+ // Insert to the database at first
+ createRepository(form.name, form.owner, form.description, form.isPrivate)
- // Insert default labels
- createLabel(loginUserName, form.name, "bug", "fc2929")
- createLabel(loginUserName, form.name, "duplicate", "cccccc")
- createLabel(loginUserName, form.name, "enhancement", "84b6eb")
- createLabel(loginUserName, form.name, "invalid", "e6e6e6")
- createLabel(loginUserName, form.name, "question", "cc317c")
- createLabel(loginUserName, form.name, "wontfix", "ffffff")
+ // Add collaborators for group repository
+ if(ownerAccount.isGroupAccount){
+ getGroupMembers(form.owner).foreach { userName =>
+ addCollaborator(form.owner, form.name, userName)
+ }
+ }
- // Create the actual repository
- val gitdir = getRepositoryDir(loginUserName, form.name)
- JGitUtil.initRepository(gitdir)
+ // Insert default labels
+ insertDefaultLabels(form.owner, form.name)
- if(form.createReadme){
- val tmpdir = getInitRepositoryDir(loginUserName, form.name)
- try {
- // Clone the repository
- Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
+ // Create the actual repository
+ val gitdir = getRepositoryDir(form.owner, form.name)
+ JGitUtil.initRepository(gitdir)
- // Create README.md
- FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
- if(form.description.nonEmpty){
- form.name + "\n" +
- "===============\n" +
- "\n" +
- form.description.get
- } else {
- form.name + "\n" +
- "===============\n"
- }, "UTF-8")
+ if(form.createReadme){
+ FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir =>
+ // Clone the repository
+ Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
- val git = Git.open(tmpdir)
- git.add.addFilepattern("README.md").call
- git.commit.setMessage("Initial commit").call
- git.push.call
+ // Create README.md
+ FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
+ if(form.description.nonEmpty){
+ form.name + "\n" +
+ "===============\n" +
+ "\n" +
+ form.description.get
+ } else {
+ form.name + "\n" +
+ "===============\n"
+ }, "UTF-8")
- } finally {
- FileUtils.deleteDirectory(tmpdir)
+ val git = Git.open(tmpdir)
+ git.add.addFilepattern("README.md").call
+ git.commit
+ .setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
+ .setMessage("Initial commit").call
+ git.push.call
+ }
+ }
+
+ // Create Wiki repository
+ createWikiRepository(loginAccount, form.owner, form.name)
+
+ // Record activity
+ recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
}
+
+ // redirect to the repository
+ redirect(s"/${form.owner}/${form.name}")
}
-
- // Create Wiki repository
- createWikiRepository(loginAccount, form.name)
-
- // Record activity
- recordCreateRepositoryActivity(loginUserName, form.name, loginUserName)
-
- // redirect to the repository
- redirect(s"/${loginUserName}/${form.name}")
})
-
+
+ get("/:owner/:repository/fork")(readableUsersOnly { repository =>
+ val loginAccount = context.loginAccount.get
+ val loginUserName = loginAccount.userName
+
+ LockUtil.lock(s"${loginUserName}/${repository.name}/create"){
+ if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){
+ // Insert to the database at first
+ val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
+ val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
+
+ createRepository(
+ repositoryName = repository.name,
+ userName = loginUserName,
+ description = repository.repository.description,
+ isPrivate = repository.repository.isPrivate,
+ originRepositoryName = Some(originRepositoryName),
+ originUserName = Some(originUserName),
+ parentRepositoryName = Some(repository.name),
+ parentUserName = Some(repository.owner)
+ )
+
+ // Insert default labels
+ insertDefaultLabels(loginUserName, repository.name)
+
+ // clone repository actually
+ JGitUtil.cloneRepository(
+ getRepositoryDir(repository.owner, repository.name),
+ getRepositoryDir(loginUserName, repository.name))
+
+ // Create Wiki repository
+ JGitUtil.cloneRepository(
+ getWikiRepositoryDir(repository.owner, repository.name),
+ getWikiRepositoryDir(loginUserName, repository.name))
+
+ // insert commit id
+ using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git =>
+ JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch =>
+ JGitUtil.getCommitLog(git, branch) match {
+ case Right((commits, _)) => commits.foreach { commit =>
+ if(!existsCommitId(loginUserName, repository.name, commit.id)){
+ insertCommitId(loginUserName, repository.name, commit.id)
+ }
+ }
+ case Left(_) => ???
+ }
+ }
+ }
+
+ // Record activity
+ recordForkActivity(repository.owner, repository.name, loginUserName)
+ }
+ // redirect to the repository
+ redirect("/%s/%s".format(loginUserName, repository.name))
+ }
+ })
+
+ private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
+ createLabel(userName, repositoryName, "bug", "fc2929")
+ createLabel(userName, repositoryName, "duplicate", "cccccc")
+ createLabel(userName, repositoryName, "enhancement", "84b6eb")
+ createLabel(userName, repositoryName, "invalid", "e6e6e6")
+ createLabel(userName, repositoryName, "question", "cc317c")
+ createLabel(userName, repositoryName, "wontfix", "ffffff")
+ }
+
+ private def existsAccount: Constraint = new Constraint(){
+ override def validate(name: String, value: String): Option[String] =
+ if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
+ }
+
/**
* Duplicate check for the repository name.
*/
private def unique: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
- getRepositoryNamesOfUser(context.loginAccount.get.userName).find(_ == value).map(_ => "Repository already exists.")
+ override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
+ params.get("owner").flatMap { userName =>
+ getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala
new file mode 100644
index 0000000..871dd34
--- /dev/null
+++ b/src/main/scala/app/DashboardController.scala
@@ -0,0 +1,109 @@
+package app
+
+import service._
+import util.{UsersAuthenticator, Keys}
+import util.Implicits._
+
+class DashboardController extends DashboardControllerBase
+ with IssuesService with PullRequestService with RepositoryService with AccountService
+ with UsersAuthenticator
+
+trait DashboardControllerBase extends ControllerBase {
+ self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
+
+ get("/dashboard/issues/repos")(usersOnly {
+ searchIssues("all")
+ })
+
+ get("/dashboard/issues/assigned")(usersOnly {
+ searchIssues("assigned")
+ })
+
+ get("/dashboard/issues/created_by")(usersOnly {
+ searchIssues("created_by")
+ })
+
+ get("/dashboard/pulls")(usersOnly {
+ searchPullRequests("created_by", None)
+ })
+
+ get("/dashboard/pulls/owned")(usersOnly {
+ searchPullRequests("created_by", None)
+ })
+
+ get("/dashboard/pulls/public")(usersOnly {
+ searchPullRequests("not_created_by", None)
+ })
+
+ get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
+ searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
+ })
+
+ private def searchIssues(filter: String) = {
+ import IssuesService._
+
+ // condition
+ val condition = session.putAndGet(Keys.Session.DashboardIssues,
+ if(request.hasQueryString) IssueSearchCondition(request)
+ else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
+ )
+
+ val userName = context.loginAccount.get.userName
+ val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
+ val filterUser = Map(filter -> userName)
+ val page = IssueSearchCondition.page(request)
+ //
+ dashboard.html.issues(
+ issues.html.listparts(
+ searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*),
+ page,
+ countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*),
+ countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*),
+ condition),
+ countIssue(condition, Map.empty, false, repositories: _*),
+ countIssue(condition, Map("assigned" -> userName), false, repositories: _*),
+ countIssue(condition, Map("created_by" -> userName), false, repositories: _*),
+ countIssueGroupByRepository(condition, filterUser, false, repositories: _*),
+ condition,
+ filter)
+
+ }
+
+ private def searchPullRequests(filter: String, repository: Option[String]) = {
+ import IssuesService._
+ import PullRequestService._
+
+ // condition
+ val condition = session.putAndGet(Keys.Session.DashboardPulls, {
+ if(request.hasQueryString) IssueSearchCondition(request)
+ else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
+ }.copy(repo = repository))
+
+ val userName = context.loginAccount.get.userName
+ val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name)
+ val filterUser = Map(filter -> userName)
+ val page = IssueSearchCondition.page(request)
+
+ val counts = countIssueGroupByRepository(
+ IssueSearchCondition().copy(state = condition.state), Map.empty, true, repositories: _*)
+
+ dashboard.html.pulls(
+ pulls.html.listparts(
+ searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, repositories: _*),
+ page,
+ countIssue(condition.copy(state = "open"), filterUser, true, repositories: _*),
+ countIssue(condition.copy(state = "closed"), filterUser, true, repositories: _*),
+ condition,
+ None,
+ false),
+ getPullRequestCountGroupByUser(condition.state == "closed", userName, None),
+ getRepositoryNamesOfUser(userName).map { RepoName =>
+ (userName, RepoName, counts.collectFirst { case (_, RepoName, count) => count }.getOrElse(0))
+ }.sortBy(_._3).reverse,
+ condition,
+ filter)
+
+ }
+
+
+}
diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala
index 5d5ed3b..6ea5fa2 100644
--- a/src/main/scala/app/FileUploadController.scala
+++ b/src/main/scala/app/FileUploadController.scala
@@ -1,6 +1,7 @@
package app
-import util.{FileUtil, FileUploadUtil}
+import _root_.util.{Keys, FileUtil}
+import util.ControlUtil._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport}
import org.apache.commons.io.FileUtils
@@ -9,18 +10,18 @@
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file as temporary file and returns the unique id.
- * You can get uploaded file using [[util.FileUploadUtil#getTemporaryFile()]] with this id.
+ * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id.
*/
-// TODO Remove temporary files at session timeout by session listener.
-class FileUploadController extends ScalatraServlet with FileUploadSupport with FlashMapSupport {
+class FileUploadController extends ScalatraServlet
+ with FileUploadSupport with FlashMapSupport with FileUploadControllerBase {
+
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
fileParams.get("file") match {
- case Some(file) if(FileUtil.isImage(file.name)) => {
- val fileId = FileUploadUtil.generateFileId
- FileUtils.writeByteArrayToFile(FileUploadUtil.getTemporaryFile(fileId), file.get)
- session += "upload_" + fileId -> file.name
+ case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId =>
+ FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get)
+ session += Keys.Session.Upload(fileId) -> file.name
Ok(fileId)
}
case None => BadRequest
diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala
index c16873b..f827492 100644
--- a/src/main/scala/app/IndexController.scala
+++ b/src/main/scala/app/IndexController.scala
@@ -1,21 +1,38 @@
package app
+import util._
import service._
+import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
- with RepositoryService with AccountService with SystemSettingsService with ActivityService
+ with RepositoryService with SystemSettingsService with ActivityService with AccountService
+with UsersAuthenticator
-trait IndexControllerBase extends ControllerBase { self: RepositoryService
- with SystemSettingsService with ActivityService =>
-
+trait IndexControllerBase extends ControllerBase {
+ self: RepositoryService with SystemSettingsService with ActivityService with AccountService
+ with UsersAuthenticator =>
+
get("/"){
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
- getAccessibleRepositories(loginAccount, baseUrl),
+ getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
- loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil)
+ loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
-}
\ No newline at end of file
+ /**
+ * JSON API for collaborator completion.
+ *
+ * TODO Move to other controller?
+ */
+ get("/_user/proposals")(usersOnly {
+ contentType = formats("json")
+ org.json4s.jackson.Serialization.write(
+ Map("options" -> getAllUsers.filter(!_.isGroupAccount).map(_.userName).toArray)
+ )
+ })
+
+
+}
diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala
index 09cc76b..98281e5 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -4,7 +4,9 @@
import service._
import IssuesService._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator}
+import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, ReadableUsersAuthenticator, Notifier, Keys}
+import util.Implicits._
+import util.ControlUtil._
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
@@ -57,98 +59,100 @@
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
- val owner = repository.owner
- val name = repository.name
- val issueId = params("id")
-
- getIssue(owner, name, issueId) map {
- issues.html.issue(
+ defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
+ getIssue(owner, name, issueId) map {
+ issues.html.issue(
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
(getCollaborators(owner, name) :+ owner).sorted,
+ getMilestonesWithIssueCount(owner, name),
+ getLabels(owner, name),
+ hasWritePermission(owner, name, context.loginAccount),
+ repository)
+ } getOrElse NotFound
+ }
+ })
+
+ get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ issues.html.create(
+ (getCollaborators(owner, name) :+ owner).sorted,
getMilestones(owner, name),
getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount),
repository)
- } getOrElse NotFound
- })
-
- get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
- val owner = repository.owner
- val name = repository.name
-
- issues.html.create(
- (getCollaborators(owner, name) :+ owner).sorted,
- getMilestones(owner, name),
- getLabels(owner, name),
- hasWritePermission(owner, name, context.loginAccount),
- repository)
+ }
})
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
- val owner = repository.owner
- val name = repository.name
- val writable = hasWritePermission(owner, name, context.loginAccount)
- val userName = context.loginAccount.get.userName
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ val writable = hasWritePermission(owner, name, context.loginAccount)
+ val userName = context.loginAccount.get.userName
- // insert issue
- val issueId = createIssue(owner, name, userName, form.title, form.content,
- if(writable) form.assignedUserName else None,
- if(writable) form.milestoneId else None)
+ // insert issue
+ val issueId = createIssue(owner, name, userName, form.title, form.content,
+ if(writable) form.assignedUserName else None,
+ if(writable) form.milestoneId else None)
- // insert labels
- if(writable){
- form.labelNames.map { value =>
- val labels = getLabels(owner, name)
- value.split(",").foreach { labelName =>
- labels.find(_.labelName == labelName).map { label =>
- registerIssueLabel(owner, name, issueId, label.labelId)
+ // insert labels
+ if(writable){
+ form.labelNames.map { value =>
+ val labels = getLabels(owner, name)
+ value.split(",").foreach { labelName =>
+ labels.find(_.labelName == labelName).map { label =>
+ registerIssueLabel(owner, name, issueId, label.labelId)
+ }
}
}
}
+
+ // record activity
+ recordCreateIssueActivity(owner, name, userName, issueId, form.title)
+
+ // notifications
+ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
+ Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
+ }
+
+ redirect(s"/${owner}/${name}/issues/${issueId}")
}
-
- // record activity
- recordCreateIssueActivity(owner, name, userName, issueId, form.title)
-
- redirect("/%s/%s/issues/%d".format(owner, name, issueId))
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
- val owner = repository.owner
- val name = repository.name
-
- getIssue(owner, name, params("id")).map { issue =>
- if(isEditable(owner, name, issue.openedUserName)){
- updateIssue(owner, name, issue.issueId, form.title, form.content)
- redirect("/%s/%s/issues/_data/%d".format(owner, name, issue.issueId))
- } else Unauthorized
- } getOrElse NotFound
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ getIssue(owner, name, params("id")).map { issue =>
+ if(isEditable(owner, name, issue.openedUserName)){
+ updateIssue(owner, name, issue.issueId, form.title, form.content)
+ redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
+ } else Unauthorized
+ } getOrElse NotFound
+ }
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, Some(form.content), repository)() map { id =>
- redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id))
+ handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, form.content, repository)() map { id =>
- redirect("/%s/%s/issues/%d#comment-%d".format(repository.owner, repository.name, form.issueId, id))
+ handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
- val owner = repository.owner
- val name = repository.name
-
- getComment(owner, name, params("id")).map { comment =>
- if(isEditable(owner, name, comment.commentedUserName)){
- updateComment(comment.commentId, form.content)
- redirect("/%s/%s/issue_comments/_data/%d".format(owner, name, comment.commentId))
- } else Unauthorized
- } getOrElse NotFound
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ getComment(owner, name, params("id")).map { comment =>
+ if(isEditable(owner, name, comment.commentedUserName)){
+ updateComment(comment.commentId, form.content)
+ redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
+ } else Unauthorized
+ } getOrElse NotFound
+ }
})
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
@@ -187,17 +191,17 @@
})
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
- val issueId = params("id").toInt
-
- registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
- issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
+ defining(params("id").toInt){ issueId =>
+ registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
+ issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
+ }
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
- val issueId = params("id").toInt
-
- deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
- issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
+ defining(params("id").toInt){ issueId =>
+ deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
+ issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
+ }
})
ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository =>
@@ -207,128 +211,148 @@
ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository =>
updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
- Ok("updated")
+ milestoneId("milestoneId").map { milestoneId =>
+ getMilestonesWithIssueCount(repository.owner, repository.name)
+ .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
+ issues.milestones.html.progress(openCount + closeCount, closeCount, false)
+ } getOrElse NotFound
+ } getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
- val action = params.get("value")
-
- executeBatch(repository) {
- handleComment(_, None, repository)( _ => action)
- }
- })
-
- post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
- val labelId = params("value").toInt
-
- executeBatch(repository) { issueId =>
- getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
- registerIssueLabel(repository.owner, repository.name, issueId, labelId)
+ defining(params.get("value")){ action =>
+ executeBatch(repository) {
+ handleComment(_, None, repository)( _ => action)
}
}
})
- post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
- val value = assignedUserName("value")
+ post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository =>
+ params("value").toIntOpt.map{ labelId =>
+ executeBatch(repository) { issueId =>
+ getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
+ registerIssueLabel(repository.owner, repository.name, issueId, labelId)
+ }
+ }
+ } getOrElse NotFound
+ })
- executeBatch(repository) {
- updateAssignedUserName(repository.owner, repository.name, _, value)
+ post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository =>
+ defining(assignedUserName("value")){ value =>
+ executeBatch(repository) {
+ updateAssignedUserName(repository.owner, repository.name, _, value)
+ }
}
})
post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository =>
- val value = milestoneId("value")
-
- executeBatch(repository) {
- updateMilestoneId(repository.owner, repository.name, _, value)
+ defining(milestoneId("value")){ value =>
+ executeBatch(repository) {
+ updateMilestoneId(repository.owner, repository.name, _, value)
+ }
}
})
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
- val milestoneId = (key: String) => params.get(key) collect { case x if x.trim != "" => x.toInt }
+ val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
- redirect("/%s/%s/issues".format(repository.owner, repository.name))
+ redirect(s"/${repository.owner}/${repository.name}/issues")
}
/**
- * @see
+ * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
- val owner = repository.owner
- val name = repository.name
- val userName = context.loginAccount.get.userName
- getIssue(owner, name, issueId.toString) map { issue =>
- val (action, recordActivity) =
- getAction(issue)
- .collect {
- case "close" => true -> (Some("close") -> Some(recordCloseIssueActivity _))
- case "reopen" => false -> (Some("reopen") -> Some(recordReopenIssueActivity _))
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ val userName = context.loginAccount.get.userName
+
+ getIssue(owner, name, issueId.toString) map { issue =>
+ val (action, recordActivity) =
+ getAction(issue)
+ .collect {
+ case "close" => true -> (Some("close") ->
+ Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
+ case "reopen" => false -> (Some("reopen") ->
+ Some(recordReopenIssueActivity _))
}
- .map { case (closed, t) =>
+ .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
- .getOrElse(None -> None)
+ .getOrElse(None -> None)
- val commentId = content
+ val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
- match {
- case (content, action) => createComment(owner, name, userName, issueId, content, action)
- }
+ match {
+ case (content, action) => createComment(owner, name, userName, issueId, content, action)
+ }
- // record activity
- content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
- recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
+ // record activity
+ content foreach {
+ (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
+ (owner, name, userName, issueId, _)
+ }
+ recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
- commentId
+ // notifications
+ Notifier() match {
+ case f =>
+ content foreach {
+ f.toNotify(repository, issueId, _){
+ Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}")
+ }
+ }
+ action foreach {
+ f.toNotify(repository, issueId, _){
+ Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}")
+ }
+ }
+ }
+
+ issue -> commentId
+ }
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
- val owner = repository.owner
- val repoName = repository.name
- val userName = if(filter != "all") Some(params("userName")) else None
- val sessionKey = "%s/%s/issues".format(owner, repoName)
+ defining(repository.owner, repository.name){ case (owner, repoName) =>
+ val filterUser = Map(filter -> params.getOrElse("userName", ""))
+ val page = IssueSearchCondition.page(request)
+ val sessionKey = Keys.Session.Issues(owner, repoName)
- val page = try {
- val i = params.getOrElse("page", "1").toInt
- if(i <= 0) 1 else i
- } catch {
- case e: NumberFormatException => 1
+ // retrieve search condition
+ val condition = session.putAndGet(sessionKey,
+ if(request.hasQueryString) IssueSearchCondition(request)
+ else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
+ )
+
+ issues.html.list(
+ searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
+ page,
+ (getCollaborators(owner, repoName) :+ owner).sorted,
+ getMilestones(owner, repoName),
+ getLabels(owner, repoName),
+ countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
+ countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
+ countIssue(condition, Map.empty, false, owner -> repoName),
+ context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
+ context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
+ countIssueGroupByLabels(owner, repoName, condition, filterUser),
+ condition,
+ filter,
+ repository,
+ hasWritePermission(owner, repoName, context.loginAccount))
}
-
- // retrieve search condition
- val condition = if(request.getQueryString == null){
- session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
- } else IssueSearchCondition(request)
-
- session.put(sessionKey, condition)
-
- issues.html.list(
- searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
- page,
- (getCollaborators(owner, repoName) :+ owner).sorted,
- getMilestones(owner, repoName).filter(_.closedDate.isEmpty),
- getLabels(owner, repoName),
- countIssue(owner, repoName, condition.copy(state = "open"), filter, userName),
- countIssue(owner, repoName, condition.copy(state = "closed"), filter, userName),
- countIssue(owner, repoName, condition, "all", None),
- context.loginAccount.map(x => countIssue(owner, repoName, condition, "assigned", Some(x.userName))),
- context.loginAccount.map(x => countIssue(owner, repoName, condition, "created_by", Some(x.userName))),
- countIssueGroupByLabels(owner, repoName, condition, filter, userName),
- condition,
- filter,
- repository,
- hasWritePermission(owner, repoName, context.loginAccount))
}
}
diff --git a/src/main/scala/app/LabelsController.scala b/src/main/scala/app/LabelsController.scala
index e666d67..1366c4c 100644
--- a/src/main/scala/app/LabelsController.scala
+++ b/src/main/scala/app/LabelsController.scala
@@ -51,7 +51,7 @@
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[^,]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
diff --git a/src/main/scala/app/MilestonesController.scala b/src/main/scala/app/MilestonesController.scala
index 55c60d0..234ca07 100644
--- a/src/main/scala/app/MilestonesController.scala
+++ b/src/main/scala/app/MilestonesController.scala
@@ -3,7 +3,8 @@
import jp.sf.amateras.scalatra.forms._
import service._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, UsersAuthenticator}
+import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
+import util.Implicits._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
@@ -39,34 +40,44 @@
})
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
- issues.milestones.html.edit(getMilestone(repository.owner, repository.name, params("milestoneId").toInt), repository)
+ params("milestoneId").toIntOpt.map{ milestoneId =>
+ issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
+ } getOrElse NotFound
})
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) =>
- getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
- updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
- redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ params("milestoneId").toIntOpt.flatMap{ milestoneId =>
+ getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
+ updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
+ redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ }
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository =>
- getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
- closeMilestone(milestone)
- redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ params("milestoneId").toIntOpt.flatMap{ milestoneId =>
+ getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
+ closeMilestone(milestone)
+ redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ }
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository =>
- getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
- openMilestone(milestone)
- redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ params("milestoneId").toIntOpt.flatMap{ milestoneId =>
+ getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
+ openMilestone(milestone)
+ redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ }
} getOrElse NotFound
})
get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository =>
- getMilestone(repository.owner, repository.name, params("milestoneId").toInt).map { milestone =>
- deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
- redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ params("milestoneId").toIntOpt.flatMap{ milestoneId =>
+ getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
+ deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
+ redirect(s"/${repository.owner}/${repository.name}/issues/milestones")
+ }
} getOrElse NotFound
})
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
new file mode 100644
index 0000000..c234bc6
--- /dev/null
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -0,0 +1,417 @@
+package app
+
+import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
+import util.Directory._
+import util.Implicits._
+import util.ControlUtil._
+import util.FileUtil._
+import service._
+import org.eclipse.jgit.api.Git
+import jp.sf.amateras.scalatra.forms._
+import org.eclipse.jgit.transport.RefSpec
+import org.apache.commons.io.FileUtils
+import scala.collection.JavaConverters._
+import org.eclipse.jgit.lib.PersonIdent
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode
+import service.IssuesService._
+import service.PullRequestService._
+import util.JGitUtil.DiffInfo
+import service.RepositoryService.RepositoryTreeNode
+import util.JGitUtil.CommitInfo
+
+class PullRequestsController extends PullRequestsControllerBase
+ with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
+ with ReferrerAuthenticator with CollaboratorsAuthenticator
+
+trait PullRequestsControllerBase extends ControllerBase {
+ self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
+ with ReferrerAuthenticator with CollaboratorsAuthenticator =>
+
+ val pullRequestForm = mapping(
+ "title" -> trim(label("Title" , text(required, maxlength(100)))),
+ "content" -> trim(label("Content", optional(text()))),
+ "targetUserName" -> trim(text(required, maxlength(100))),
+ "targetBranch" -> trim(text(required, maxlength(100))),
+ "requestUserName" -> trim(text(required, maxlength(100))),
+ "requestBranch" -> trim(text(required, maxlength(100))),
+ "commitIdFrom" -> trim(text(required, maxlength(40))),
+ "commitIdTo" -> trim(text(required, maxlength(40)))
+ )(PullRequestForm.apply)
+
+ val mergeForm = mapping(
+ "message" -> trim(label("Message", text(required)))
+ )(MergeForm.apply)
+
+ case class PullRequestForm(
+ title: String,
+ content: Option[String],
+ targetUserName: String,
+ targetBranch: String,
+ requestUserName: String,
+ requestBranch: String,
+ commitIdFrom: String,
+ commitIdTo: String)
+
+ case class MergeForm(message: String)
+
+ get("/:owner/:repository/pulls")(referrersOnly { repository =>
+ searchPullRequests(None, repository)
+ })
+
+ get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
+ searchPullRequests(Some(params("userName")), repository)
+ })
+
+ get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
+ params("id").toIntOpt.flatMap{ issueId =>
+ val owner = repository.owner
+ val name = repository.name
+ getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
+ using(Git.open(getRepositoryDir(owner, name))){ git =>
+ val (commits, diffs) =
+ getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
+
+ pulls.html.pullreq(
+ issue, pullreq,
+ getComments(owner, name, issueId),
+ (getCollaborators(owner, name) :+ owner).sorted,
+ getMilestonesWithIssueCount(owner, name),
+ commits,
+ diffs,
+ hasWritePermission(owner, name, context.loginAccount),
+ repository)
+ }
+ }
+ } getOrElse NotFound
+ })
+
+ ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
+ params("id").toIntOpt.flatMap{ issueId =>
+ val owner = repository.owner
+ val name = repository.name
+ getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
+ pulls.html.mergeguide(
+ checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch),
+ pullreq,
+ s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
+ }
+ } getOrElse NotFound
+ })
+
+ post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
+ params("id").toIntOpt.flatMap{ issueId =>
+ val owner = repository.owner
+ val name = repository.name
+ LockUtil.lock(s"${owner}/${name}/merge"){
+ getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
+ val remote = getRepositoryDir(owner, name)
+ withTmpDir(new java.io.File(getTemporaryDir(owner, name), s"merge-${issueId}")){ tmpdir =>
+ using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(pullreq.branch).call){ git =>
+
+ // mark issue as merged and close.
+ val loginAccount = context.loginAccount.get
+ createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
+ createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
+ updateClosed(owner, name, issueId, true)
+
+ // record activity
+ recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
+
+ // fetch pull request to temporary working repository
+ val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
+
+ git.fetch
+ .setRemote(getRepositoryDir(owner, name).toURI.toString)
+ .setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call
+
+ // merge pull request
+ git.checkout.setName(pullreq.branch).call
+
+ val result = git.merge
+ .include(git.getRepository.resolve(pullRequestBranchName))
+ .setFastForward(FastForwardMode.NO_FF)
+ .setCommit(false)
+ .call
+
+ if(result.getConflicts != null){
+ throw new RuntimeException("This pull request can't merge automatically.")
+ }
+
+ // merge commit
+ git.getRepository.writeMergeCommitMsg(
+ s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
+ + form.message)
+
+ git.commit
+ .setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
+ .call
+
+ // push
+ git.push.call
+
+ val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
+ pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
+
+ commits.flatten.foreach { commit =>
+ if(!existsCommitId(owner, name, commit.id)){
+ insertCommitId(owner, name, commit.id)
+ }
+ }
+
+ // notifications
+ Notifier().toNotify(repository, issueId, "merge"){
+ Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}")
+ }
+
+ redirect(s"/${owner}/${name}/pull/${issueId}")
+ }
+ }
+ }
+ }
+ } getOrElse NotFound
+ })
+
+ get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
+ (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
+ case (Some(originUserName), Some(originRepositoryName)) => {
+ getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
+ using(
+ Git.open(getRepositoryDir(originUserName, originRepositoryName)),
+ Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
+ ){ (oldGit, newGit) =>
+ val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
+ val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
+
+ redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
+ }
+ } getOrElse NotFound
+ }
+ case _ => {
+ using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
+ JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
+ redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
+ } getOrElse {
+ redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}")
+ }
+ }
+ }
+ }
+ })
+
+ get("/:owner/:repository/compare/*...*")(referrersOnly { repository =>
+ val Seq(origin, forked) = multiParams("splat")
+ val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
+ val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
+
+ (getRepository(originOwner, repository.name, baseUrl),
+ getRepository(forkedOwner, repository.name, baseUrl)) match {
+ case (Some(originRepository), Some(forkedRepository)) => {
+ using(
+ Git.open(getRepositoryDir(originOwner, repository.name)),
+ Git.open(getRepositoryDir(forkedOwner, repository.name))
+ ){ case (oldGit, newGit) =>
+ val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
+ val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
+
+ val forkedId = getForkedCommitId(oldGit, newGit,
+ originOwner, repository.name, originBranch,
+ forkedOwner, repository.name, forkedBranch)
+
+ val oldId = oldGit.getRepository.resolve(forkedId)
+ val newId = newGit.getRepository.resolve(forkedBranch)
+
+ val (commits, diffs) = getRequestCompareInfo(
+ originOwner, repository.name, oldId.getName,
+ forkedOwner, repository.name, newId.getName)
+
+ pulls.html.compare(
+ commits,
+ diffs,
+ repository.repository.originUserName.map { userName =>
+ userName :: getForkedRepositories(userName, repository.name)
+ } getOrElse List(repository.owner),
+ originBranch,
+ forkedBranch,
+ oldId.getName,
+ newId.getName,
+ repository,
+ originRepository,
+ forkedRepository,
+ hasWritePermission(repository.owner, repository.name, context.loginAccount))
+ }
+ }
+ case _ => NotFound
+ }
+ })
+
+ ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { repository =>
+ val Seq(origin, forked) = multiParams("splat")
+ val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner)
+ val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner)
+
+ (getRepository(originOwner, repository.name, baseUrl),
+ getRepository(forkedOwner, repository.name, baseUrl)) match {
+ case (Some(originRepository), Some(forkedRepository)) => {
+ using(
+ Git.open(getRepositoryDir(originOwner, repository.name)),
+ Git.open(getRepositoryDir(forkedOwner, repository.name))
+ ){ case (oldGit, newGit) =>
+ val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
+ val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
+
+ pulls.html.mergecheck(
+ checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch))
+ }
+ }
+ case _ => NotFound()
+ }
+ })
+
+ post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
+ val loginUserName = context.loginAccount.get.userName
+
+ val issueId = createIssue(
+ owner = repository.owner,
+ repository = repository.name,
+ loginUser = loginUserName,
+ title = form.title,
+ content = form.content,
+ assignedUserName = None,
+ milestoneId = None,
+ isPullRequest = true)
+
+ createPullRequest(
+ originUserName = repository.owner,
+ originRepositoryName = repository.name,
+ issueId = issueId,
+ originBranch = form.targetBranch,
+ requestUserName = form.requestUserName,
+ requestRepositoryName = repository.name,
+ requestBranch = form.requestBranch,
+ commitIdFrom = form.commitIdFrom,
+ commitIdTo = form.commitIdTo)
+
+ // fetch requested branch
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ git.fetch
+ .setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString)
+ .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
+ .call
+ }
+
+ // record activity
+ recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
+
+ // notifications
+ Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
+ Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
+ }
+
+ redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
+ })
+
+ /**
+ * Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
+ */
+ private def checkConflict(userName: String, repositoryName: String, branch: String,
+ requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
+ // TODO Are there more quick way?
+ LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){
+ val remote = getRepositoryDir(userName, repositoryName)
+ withTmpDir(new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")){ tmpdir =>
+ using(Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).setBranch(branch).call){ git =>
+
+ git.checkout.setName(branch).call
+
+ git.fetch
+ .setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
+ .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call
+
+ val result = git.merge
+ .include(git.getRepository.resolve("FETCH_HEAD"))
+ .setCommit(false).call
+
+ result.getConflicts != null
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses branch identifier and extracts owner and branch name as tuple.
+ *
+ * - "owner:branch" to ("owner", "branch")
+ * - "branch" to ("defaultOwner", "branch")
+ */
+ private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
+ if(value.contains(':')){
+ val array = value.split(":")
+ (array(0), array(1))
+ } else {
+ (defaultOwner, value)
+ }
+
+ /**
+ * Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list.
+ */
+ private def getRepositoryNames(node: RepositoryTreeNode): List[String] =
+ node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten
+
+ /**
+ * Returns the identifier of the root commit (or latest merge commit) of the specified branch.
+ */
+ private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String,
+ requestUserName: String, requestRepositoryName: String, requestBranch: String): String =
+ JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit =>
+ existsCommitId(userName, repositoryName, commit.getName) &&
+ JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch)
+ }.head.id
+
+ private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
+ requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
+
+ using(
+ Git.open(getRepositoryDir(userName, repositoryName)),
+ Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
+ ){ (oldGit, newGit) =>
+ val oldId = oldGit.getRepository.resolve(branch)
+ val newId = newGit.getRepository.resolve(requestCommitId)
+
+ val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
+ new CommitInfo(revCommit)
+ }.toList.splitWith{ (commit1, commit2) =>
+ view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
+ }
+
+ val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
+
+ (commits, diffs)
+ }
+ }
+
+ private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
+ defining(repository.owner, repository.name){ case (owner, repoName) =>
+ val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
+ val page = IssueSearchCondition.page(request)
+ val sessionKey = Keys.Session.Pulls(owner, repoName)
+
+ // retrieve search condition
+ val condition = session.putAndGet(sessionKey,
+ if(request.hasQueryString) IssueSearchCondition(request)
+ else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
+ )
+
+ pulls.html.list(
+ searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
+ getPullRequestCountGroupByUser(condition.state == "closed", owner, Some(repoName)),
+ userName,
+ page,
+ countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
+ countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
+ countIssue(condition, Map.empty, true, owner -> repoName),
+ condition,
+ repository,
+ hasWritePermission(owner, repoName, context.loginAccount))
+ }
+
+}
diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala
index 08f6474..a9285fd 100644
--- a/src/main/scala/app/RepositorySettingsController.scala
+++ b/src/main/scala/app/RepositorySettingsController.scala
@@ -6,13 +6,20 @@
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.FlashMapSupport
+import service.WebHookService.WebHookPayload
+import util.JGitUtil.CommitInfo
+import util.ControlUtil._
+import org.eclipse.jgit.api.Git
class RepositorySettingsController extends RepositorySettingsControllerBase
- with RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator
+ with RepositoryService with AccountService with WebHookService
+ with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSupport {
- self: RepositoryService with AccountService with OwnerAuthenticator with UsersAuthenticator =>
+ self: RepositoryService with AccountService with WebHookService
+ with OwnerAuthenticator with UsersAuthenticator =>
+ // for repository options
case class OptionsForm(description: Option[String], defaultBranch: String, isPrivate: Boolean)
val optionsForm = mapping(
@@ -20,13 +27,21 @@
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
-
+
+ // for collaborator addition
case class CollaboratorForm(userName: String)
val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply)
+ // for web hook url addition
+ case class WebHookForm(url: String)
+
+ val webHookForm = mapping(
+ "url" -> trim(label("url", text(required, webHook)))
+ )(WebHookForm.apply)
+
/**
* Redirect to the Options page.
*/
@@ -45,7 +60,15 @@
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
- saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate)
+ saveRepositoryOptions(
+ repository.owner,
+ repository.name,
+ form.description,
+ form.defaultBranch,
+ repository.repository.parentUserName.map { _ =>
+ repository.repository.isPrivate
+ } getOrElse form.isPrivate
+ )
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/options")
})
@@ -54,22 +77,19 @@
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
- settings.html.collaborators(getCollaborators(repository.owner, repository.name), repository)
- })
-
- /**
- * JSON API for collaborator completion.
- */
- get("/:owner/:repository/settings/collaborators/proposals")(usersOnly {
- contentType = formats("json")
- org.json4s.jackson.Serialization.write(Map("options" -> getAllUsers.map(_.userName).toArray))
+ settings.html.collaborators(
+ getCollaborators(repository.owner, repository.name),
+ getAccountByUserName(repository.owner).get.isGroupAccount,
+ repository)
})
/**
* Add the collaborator.
*/
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) =>
- addCollaborator(repository.owner, repository.name, form.userName)
+ if(!getAccountByUserName(repository.owner).get.isGroupAccount){
+ addCollaborator(repository.owner, repository.name, form.userName)
+ }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
@@ -77,11 +97,63 @@
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
- removeCollaborator(repository.owner, repository.name, params("name"))
+ if(!getAccountByUserName(repository.owner).get.isGroupAccount){
+ removeCollaborator(repository.owner, repository.name, params("name"))
+ }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
+ * Display the web hook page.
+ */
+ get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
+ settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
+ })
+
+ /**
+ * Add the web hook URL.
+ */
+ post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
+ addWebHookURL(repository.owner, repository.name, form.url)
+ redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
+ })
+
+ /**
+ * Delete the web hook URL.
+ */
+ get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
+ deleteWebHookURL(repository.owner, repository.name, params("url"))
+ redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
+ })
+
+ /**
+ * Send the test request to registered web hook URLs.
+ */
+ get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ import scala.collection.JavaConverters._
+ val commits = git.log
+ .add(git.getRepository.resolve(repository.repository.defaultBranch))
+ .setMaxCount(3)
+ .call.iterator.asScala.map(new CommitInfo(_))
+
+ val webHookURLs = getWebHookURLs(repository.owner, repository.name)
+ if(webHookURLs.nonEmpty){
+ callWebHook(repository.owner, repository.name, webHookURLs,
+ WebHookPayload(
+ git,
+ "refs/heads/" + repository.repository.defaultBranch,
+ repository,
+ commits.toList,
+ getAccountByUserName(repository.owner).get))
+ }
+
+ flash += "info" -> "Test payload deployed!"
+ }
+ redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
+ })
+
+ /**
* Display the delete repository page.
*/
get("/:owner/:repository/settings/delete")(ownerOnly {
@@ -102,18 +174,24 @@
})
/**
+ * Provides duplication check for web hook url.
+ */
+ private def webHook: Constraint = new Constraint(){
+ override def validate(name: String, value: String): Option[String] =
+ getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
+ }
+
+ /**
* Provides Constraint to validate the collaborator name.
*/
private def collaborator: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] = {
- val paths = request.getRequestURI.split("/")
+ override def validate(name: String, value: String): Option[String] =
getAccountByUserName(value) match {
case None => Some("User does not exist.")
- case Some(x) if(x.userName == paths(1) || getCollaborators(paths(1), paths(2)).contains(x.userName))
+ case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.")
case _ => None
}
- }
}
}
\ No newline at end of file
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index 43cb126..638478d 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -2,7 +2,8 @@
import util.Directory._
import util.Implicits._
-import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil}
+import util.ControlUtil._
+import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil}
import service._
import org.scalatra._
import java.io.File
@@ -10,6 +11,7 @@
import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
+import org.eclipse.jgit.api.errors.RefNotFoundException
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ReferrerAuthenticator
@@ -38,48 +40,28 @@
})
/**
- * Displays the file list of the repository root and the specified branch.
- */
- get("/:owner/:repository/tree/:id")(referrersOnly {
- fileList(_, params("id"))
- })
-
- /**
* Displays the file list of the specified path and branch.
*/
- get("/:owner/:repository/tree/:id/*")(referrersOnly {
- fileList(_, params("id"), multiParams("splat").head)
- })
-
- /**
- * Displays the commit list of the specified branch.
- */
- get("/:owner/:repository/commits/:branch")(referrersOnly { repository =>
- val branchName = params("branch")
- val page = params.getOrElse("page", "1").toInt
- JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
- JGitUtil.getCommitLog(git, branchName, page, 30) match {
- case Right((logs, hasNext)) =>
- repo.html.commits(Nil, branchName, repository, logs.splitWith{ (commit1, commit2) =>
- view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
- }, page, hasNext)
- case Left(_) => NotFound
- }
+ get("/:owner/:repository/tree/*")(referrersOnly { repository =>
+ val (id, path) = splitPath(repository, multiParams("splat").head)
+ if(path.isEmpty){
+ fileList(repository, id)
+ } else {
+ fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
- get("/:owner/:repository/commits/:branch/*")(referrersOnly { repository =>
- val branchName = params("branch")
- val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
- val page = params.getOrElse("page", "1").toInt
+ get("/:owner/:repository/commits/*")(referrersOnly { repository =>
+ val (branchName, path) = splitPath(repository, multiParams("splat").head)
+ val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
- JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
- repo.html.commits(path.split("/").toList, branchName, repository,
+ repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
}, page, hasNext)
@@ -91,12 +73,11 @@
/**
* Displays the file content of the specified branch or commit.
*/
- get("/:owner/:repository/blob/:id/*")(referrersOnly { repository =>
- val id = params("id") // branch name or commit id
- val raw = params.get("raw").getOrElse("false").toBoolean
- val path = multiParams("splat").head //.replaceFirst("^tree/.+?/", "")
+ get("/:owner/:repository/blob/*")(referrersOnly { repository =>
+ val (id, path) = splitPath(repository, multiParams("splat").head)
+ val raw = params.get("raw").getOrElse("false").toBoolean
- JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@scala.annotation.tailrec
@@ -105,19 +86,18 @@
case true => getPathObjectId(path, walk)
}
- val treeWalk = new TreeWalk(git.getRepository)
- val objectId = try {
+ val objectId = using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
getPathObjectId(path, treeWalk)
- } finally {
- treeWalk.release
}
if(raw){
// Download
- contentType = "application/octet-stream"
- JGitUtil.getContent(git, objectId, false).get
+ defining(JGitUtil.getContent(git, objectId, false).get){ bytes =>
+ contentType = FileUtil.getContentType(path, bytes)
+ bytes
+ }
} else {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
@@ -127,7 +107,7 @@
val content = if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
- JGitUtil.ContentInfo("text", bytes.map(new String(_, "UTF-8")))
+ JGitUtil.ContentInfo("text", bytes.map(StringUtil.convertFromByteArray))
} else {
// binary
JGitUtil.ContentInfo("binary", None)
@@ -148,22 +128,39 @@
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id")
- JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
- val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
-
- repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
- JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
- repository, JGitUtil.getDiffs(git, id))
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
+ JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
+ repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
+ JGitUtil.getBranchesOfCommit(git, revCommit.getName),
+ JGitUtil.getTagsOfCommit(git, revCommit.getName),
+ repository, diffs, oldCommitId)
+ }
+ }
}
})
/**
+ * Displays branches.
+ */
+ get("/:owner/:repository/branches")(referrersOnly { repository =>
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
+ // retrieve latest update date of each branch
+ val branchInfo = repository.branchList.map { branchName =>
+ val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
+ (branchName, revCommit.getCommitterIdent.getWhen)
+ }
+ repo.html.branches(branchInfo, repository)
+ }
+ })
+
+ /**
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
repo.html.tags(_)
})
-
+
/**
* Download repository contents as an archive.
*/
@@ -180,11 +177,12 @@
// clone the repository
val cloneDir = new File(workDir, revision)
- JGitUtil.withGit(Git.cloneRepository
+ using(Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).toURI.toString)
.setDirectory(cloneDir)
+ .setBranch(revision)
.call){ git =>
-
+
// checkout the specified revision
git.checkout.setName(revision).call
}
@@ -202,7 +200,29 @@
BadRequest
}
})
+
+ get("/:owner/:repository/network/members")(referrersOnly { repository =>
+ repo.html.forked(
+ getRepository(
+ repository.repository.originUserName.getOrElse(repository.owner),
+ repository.repository.originRepositoryName.getOrElse(repository.name),
+ baseUrl),
+ getForkedRepositories(
+ repository.repository.originUserName.getOrElse(repository.owner),
+ repository.repository.originRepositoryName.getOrElse(repository.name)),
+ repository)
+ })
+ private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
+ val id = repository.branchList.collectFirst {
+ case branch if(path == branch || path.startsWith(branch + "/")) => branch
+ } orElse repository.tags.collectFirst {
+ case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
+ } orElse Some(path.split("/")(0)) get
+
+ (id, path.substring(id.length).replaceFirst("^/", ""))
+ }
+
/**
* Provides HTML of the file list.
*
@@ -215,26 +235,26 @@
if(repository.commitCount == 0){
repo.html.guide(repository)
} else {
- JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git =>
+ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
- revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) =>
- val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
-
+ JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
+ defining(JGitUtil.getRevCommitFromId(git, objectId)){ revCommit =>
// get files
- val files = JGitUtil.getFileList(git, revision, path)
- // process README.md
- val readme = files.find(_.name == "README.md").map { file =>
- new String(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get, "UTF-8")
- }
+ val files = JGitUtil.getFileList(git, revision, path)
+ // process README.md
+ val readme = files.find(_.name == "README.md").map { file =>
+ StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
+ }
- repo.html.files(revision, repository,
- if(path == ".") Nil else path.split("/").toList, // current path
- new JGitUtil.CommitInfo(revCommit), // latest commit
- files, readme)
+ repo.html.files(revision, repository,
+ if(path == ".") Nil else path.split("/").toList, // current path
+ new JGitUtil.CommitInfo(revCommit), // latest commit
+ files, readme)
+ }
} getOrElse NotFound
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala
new file mode 100644
index 0000000..99ea743
--- /dev/null
+++ b/src/main/scala/app/SearchController.scala
@@ -0,0 +1,52 @@
+package app
+
+import util._
+import ControlUtil._
+import service._
+import jp.sf.amateras.scalatra.forms._
+
+class SearchController extends SearchControllerBase
+with RepositoryService with AccountService with SystemSettingsService with ActivityService
+with RepositorySearchService with IssuesService
+with ReferrerAuthenticator
+
+trait SearchControllerBase extends ControllerBase { self: RepositoryService
+ with SystemSettingsService with ActivityService with RepositorySearchService
+ with ReferrerAuthenticator =>
+
+ val searchForm = mapping(
+ "query" -> trim(text(required)),
+ "owner" -> trim(text(required)),
+ "repository" -> trim(text(required))
+ )(SearchForm.apply)
+
+ case class SearchForm(query: String, owner: String, repository: String)
+
+ post("/search", searchForm){ form =>
+ redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
+ }
+
+ get("/:owner/:repository/search")(referrersOnly { repository =>
+ defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
+ val page = try {
+ val i = params.getOrElse("page", "1").toInt
+ if(i <= 0) 1 else i
+ } catch {
+ case e: NumberFormatException => 1
+ }
+
+ target.toLowerCase match {
+ case "issue" => search.html.issues(
+ searchIssues(repository.owner, repository.name, query),
+ countFiles(repository.owner, repository.name, query),
+ query, page, repository)
+
+ case _ => search.html.code(
+ searchFiles(repository.owner, repository.name, query),
+ countIssues(repository.owner, repository.name, query),
+ query, page, repository)
+ }
+ }
+ })
+
+}
diff --git a/src/main/scala/app/SignInController.scala b/src/main/scala/app/SignInController.scala
index becc3b2..7d4b2a9 100644
--- a/src/main/scala/app/SignInController.scala
+++ b/src/main/scala/app/SignInController.scala
@@ -1,8 +1,9 @@
package app
import service._
-import util.StringUtil._
import jp.sf.amateras.scalatra.forms._
+import util.Implicits._
+import util.Keys
class SignInController extends SignInControllerBase with SystemSettingsService with AccountService
@@ -16,27 +17,17 @@
)(SignInForm.apply)
get("/signin"){
- val queryString = request.getQueryString
- if(queryString != null && queryString.startsWith("/")){
- session.setAttribute("REDIRECT", queryString)
+ val redirect = params.get("redirect")
+ if(redirect.isDefined && redirect.get.startsWith("/")){
+ session.setAttribute(Keys.Session.Redirect, redirect.get)
}
html.signin(loadSystemSettings())
}
post("/signin", form){ form =>
- val account = getAccountByUserName(form.userName)
- if(account.isEmpty || account.get.password != sha1(form.password)){
- redirect("/signin")
- } else {
- session.setAttribute("LOGIN_ACCOUNT", account.get)
- updateLastLoginDate(account.get.userName)
-
- session.get("REDIRECT").map { redirectUrl =>
- session.removeAttribute("REDIRECT")
- redirect(redirectUrl.asInstanceOf[String])
- }.getOrElse {
- redirect("/")
- }
+ authenticate(loadSystemSettings(), form.userName, form.password) match {
+ case Some(account) => signin(account)
+ case None => redirect("/signin")
}
}
@@ -45,4 +36,22 @@
redirect("/")
}
+ /**
+ * Set account information into HttpSession and redirect.
+ */
+ private def signin(account: model.Account) = {
+ session.setAttribute(Keys.Session.LoginAccount, account)
+ updateLastLoginDate(account.userName)
+
+ session.getAndRemove[String](Keys.Session.Redirect).map { redirectUrl =>
+ if(redirectUrl.replaceFirst("/$", "") == request.getContextPath){
+ redirect("/")
+ } else {
+ redirect(redirectUrl)
+ }
+ }.getOrElse {
+ redirect("/")
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala
index 6416374..9466a83 100644
--- a/src/main/scala/app/SystemSettingsController.scala
+++ b/src/main/scala/app/SystemSettingsController.scala
@@ -12,11 +12,30 @@
trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport {
self: SystemSettingsService with AccountService with AdminAuthenticator =>
- private case class SystemSettingsForm(allowAccountRegistration: Boolean)
-
private val form = mapping(
- "allowAccountRegistration" -> trim(label("Account registration", boolean()))
- )(SystemSettingsForm.apply)
+ "allowAccountRegistration" -> trim(label("Account registration", boolean())),
+ "gravatar" -> trim(label("Gravatar", boolean())),
+ "notification" -> trim(label("Notification", boolean())),
+ "smtp" -> optionalIfNotChecked("notification", mapping(
+ "host" -> trim(label("SMTP Host", text(required))),
+ "port" -> trim(label("SMTP Port", optional(number()))),
+ "user" -> trim(label("SMTP User", optional(text()))),
+ "password" -> trim(label("SMTP Password", optional(text()))),
+ "ssl" -> trim(label("Enable SSL", optional(boolean()))),
+ "fromAddress" -> trim(label("FROM Address", optional(text()))),
+ "fromName" -> trim(label("FROM Name", optional(text())))
+ )(Smtp.apply)),
+ "ldapAuthentication" -> trim(label("LDAP", boolean())),
+ "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
+ "host" -> trim(label("LDAP host", text(required))),
+ "port" -> trim(label("LDAP port", optional(number()))),
+ "bindDN" -> trim(label("Bind DN", optional(text()))),
+ "bindPassword" -> trim(label("Bind Password", optional(text()))),
+ "baseDN" -> trim(label("Base DN", text(required))),
+ "userNameAttribute" -> trim(label("User name attribute", text(required))),
+ "mailAttribute" -> trim(label("Mail address attribute", text(required)))
+ )(Ldap.apply))
+ )(SystemSettings.apply)
get("/admin/system")(adminOnly {
@@ -24,7 +43,7 @@
})
post("/admin/system", form)(adminOnly { form =>
- saveSystemSettings(SystemSettings(form.allowAccountRegistration))
+ saveSystemSettings(form)
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala
index e5673d4..2a44cb9 100644
--- a/src/main/scala/app/UserManagementController.scala
+++ b/src/main/scala/app/UserManagementController.scala
@@ -1,67 +1,96 @@
package app
import service._
-import util.{FileUploadUtil, FileUtil, AdminAuthenticator}
+import util.AdminAuthenticator
import util.StringUtil._
+import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
-import org.apache.commons.io.FileUtils
-import util.Directory._
-import scala.Some
-class UserManagementController extends UserManagementControllerBase with AccountService with AdminAuthenticator
+class UserManagementController extends UserManagementControllerBase
+ with AccountService with RepositoryService with AdminAuthenticator
trait UserManagementControllerBase extends AccountManagementControllerBase {
- self: AccountService with AdminAuthenticator =>
+ self: AccountService with RepositoryService with AdminAuthenticator =>
- case class UserNewForm(userName: String, password: String, mailAddress: String, isAdmin: Boolean,
+ case class NewUserForm(userName: String, password: String, fullName: String,
+ mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String])
- case class UserEditForm(userName: String, password: Option[String], mailAddress: String, isAdmin: Boolean,
+ case class EditUserForm(userName: String, password: Option[String], fullName: String,
+ mailAddress: String, isAdmin: Boolean,
url: Option[String], fileId: Option[String], clearImage: Boolean)
- val newForm = mapping(
+ case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
+ memberNames: Option[String])
+
+ case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
+ memberNames: Option[String], clearImage: Boolean)
+
+ val newUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
+ "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text())))
- )(UserNewForm.apply)
+ )(NewUserForm.apply)
- val editForm = mapping(
+ val editUserForm = mapping(
"userName" -> trim(label("Username" , text(required, maxlength(100), identifier))),
"password" -> trim(label("Password" , optional(text(maxlength(20))))),
+ "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))),
"isAdmin" -> trim(label("User Type" , boolean())),
"url" -> trim(label("URL" , optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" , optional(text()))),
"clearImage" -> trim(label("Clear image" , boolean()))
- )(UserEditForm.apply)
-
+ )(EditUserForm.apply)
+
+ val newGroupForm = mapping(
+ "groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier, uniqueUserName))),
+ "url" -> trim(label("URL" , optional(text(maxlength(200))))),
+ "fileId" -> trim(label("File ID" , optional(text()))),
+ "memberNames" -> trim(label("Member Names" , optional(text())))
+ )(NewGroupForm.apply)
+
+ val editGroupForm = mapping(
+ "groupName" -> trim(label("Group name" , text(required, maxlength(100), identifier))),
+ "url" -> trim(label("URL" , optional(text(maxlength(200))))),
+ "fileId" -> trim(label("File ID" , optional(text()))),
+ "memberNames" -> trim(label("Member Names" , optional(text()))),
+ "clearImage" -> trim(label("Clear image" , boolean()))
+ )(EditGroupForm.apply)
+
get("/admin/users")(adminOnly {
- admin.users.html.list(getAllUsers())
+ val users = getAllUsers()
+ val members = users.collect { case account if(account.isGroupAccount) =>
+ account.userName -> getGroupMembers(account.userName)
+ }.toMap
+ admin.users.html.list(users, members)
})
- get("/admin/users/_new")(adminOnly {
- admin.users.html.edit(None)
+ get("/admin/users/_newuser")(adminOnly {
+ admin.users.html.user(None)
})
- post("/admin/users/_new", newForm)(adminOnly { form =>
- createAccount(form.userName, sha1(form.password), form.mailAddress, form.isAdmin, form.url)
+ post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
+ createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
updateImage(form.userName, form.fileId, false)
redirect("/admin/users")
})
- get("/admin/users/:userName/_edit")(adminOnly {
+ get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName")
- admin.users.html.edit(getAccountByUserName(userName))
+ admin.users.html.user(getAccountByUserName(userName))
})
- post("/admin/users/:name/_edit", editForm)(adminOnly { form =>
+ post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { account =>
updateAccount(getAccountByUserName(userName).get.copy(
password = form.password.map(sha1).getOrElse(account.password),
+ fullName = form.fullName,
mailAddress = form.mailAddress,
isAdmin = form.isAdmin,
url = form.url))
@@ -71,5 +100,46 @@
} getOrElse NotFound
})
-
-}
\ No newline at end of file
+
+ get("/admin/users/_newgroup")(adminOnly {
+ admin.users.html.group(None, Nil)
+ })
+
+ post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
+ createGroup(form.groupName, form.url)
+ updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil))
+ updateImage(form.groupName, form.fileId, false)
+ redirect("/admin/users")
+ })
+
+ get("/admin/users/:groupName/_editgroup")(adminOnly {
+ defining(params("groupName")){ groupName =>
+ admin.users.html.group(getAccountByUserName(groupName), getGroupMembers(groupName))
+ }
+ })
+
+ post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
+ defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) =>
+ getAccountByUserName(groupName).map { account =>
+ updateGroup(groupName, form.url)
+ updateGroupMembers(form.groupName, memberNames)
+
+ getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
+ removeCollaborators(form.groupName, repositoryName)
+ memberNames.foreach { userName =>
+ addCollaborator(form.groupName, repositoryName, userName)
+ }
+ }
+
+ updateImage(form.groupName, form.fileId, form.clearImage)
+ redirect("/admin/users")
+
+ } getOrElse NotFound
+ }
+ })
+
+ post("/admin/users/_usercheck")(adminOnly {
+ getAccountByUserName(params("userName")).isDefined
+ })
+
+}
diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala
index 932f9e2..4d141de 100644
--- a/src/main/scala/app/WikiController.scala
+++ b/src/main/scala/app/WikiController.scala
@@ -1,32 +1,39 @@
package app
import service._
-import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil}
+import util._
import util.Directory._
+import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
+import org.eclipse.jgit.api.Git
+import org.scalatra.FlashMapSupport
+import service.WikiService.WikiPageInfo
+import scala.Some
class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator
-trait WikiControllerBase extends ControllerBase {
+trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator =>
- case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String)
+ case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
val newForm = mapping(
- "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
- "content" -> trim(label("Content" , text(required))),
- "message" -> trim(label("Message" , optional(text()))),
- "currentPageName" -> trim(label("Current page name" , text()))
+ "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
+ "content" -> trim(label("Content" , text(required, conflictForNew))),
+ "message" -> trim(label("Message" , optional(text()))),
+ "currentPageName" -> trim(label("Current page name" , text())),
+ "id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply)
val editForm = mapping(
- "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
- "content" -> trim(label("Content" , text(required))),
- "message" -> trim(label("Message" , optional(text()))),
- "currentPageName" -> trim(label("Current page name" , text(required)))
+ "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
+ "content" -> trim(label("Content" , text(required, conflictForEdit))),
+ "message" -> trim(label("Message" , optional(text()))),
+ "currentPageName" -> trim(label("Current page name" , text(required))),
+ "id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository =>
@@ -40,13 +47,13 @@
getWikiPage(repository.owner, repository.name, pageName).map { page =>
wiki.html.page(pageName, page, repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
- } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${pageName}/_edit") // TODO URLEncode
+ } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
- JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
+ using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository)
case Left(_) => NotFound
@@ -56,36 +63,60 @@
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
- val commitId = params("commitId").split("\\.\\.\\.")
+ val Array(from, to) = params("commitId").split("\\.\\.\\.")
- JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
- wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository)
+ using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
+ wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
+ hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
- val commitId = params("commitId").split("\\.\\.\\.")
+ val Array(from, to) = params("commitId").split("\\.\\.\\.")
- JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
- wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository)
+ using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
+ wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
+ hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
})
-
+
+ get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
+ val pageName = StringUtil.urlDecode(params("page"))
+ val Array(from, to) = params("commitId").split("\\.\\.\\.")
+
+ if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
+ redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
+ } else {
+ flash += "info" -> "This patch was not able to be reversed."
+ redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
+ }
+ })
+
+ get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
+ val Array(from, to) = params("commitId").split("\\.\\.\\.")
+
+ if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
+ redirect(s"/${repository.owner}/${repository.name}/wiki/}")
+ } else {
+ flash += "info" -> "This patch was not able to be reversed."
+ redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
+ }
+ })
+
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
- val loginAccount = context.loginAccount.get
-
- saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
- form.content, loginAccount, form.message.getOrElse(""))
-
- updateLastActivityDate(repository.owner, repository.name)
- recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
-
- redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
+ defining(context.loginAccount.get){ loginAccount =>
+ saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
+ form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
+ updateLastActivityDate(repository.owner, repository.name)
+ recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
+ }
+ redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
+ }
})
get("/:owner/:repository/wiki/_new")(collaboratorsOnly {
@@ -93,24 +124,26 @@
})
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
- val loginAccount = context.loginAccount.get
-
- saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
- form.content, context.loginAccount.get, form.message.getOrElse(""))
-
- updateLastActivityDate(repository.owner, repository.name)
- recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
+ defining(context.loginAccount.get){ loginAccount =>
+ saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
+ form.content, loginAccount, form.message.getOrElse(""), None)
- redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
+ updateLastActivityDate(repository.owner, repository.name)
+ recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
+
+ redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
+ }
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
- val pageName = StringUtil.urlDecode(params("page"))
-
- deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}")
- updateLastActivityDate(repository.owner, repository.name)
+ val pageName = StringUtil.urlDecode(params("page"))
- redirect(s"/${repository.owner}/${repository.name}/wiki")
+ defining(context.loginAccount.get){ loginAccount =>
+ deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
+ updateLastActivityDate(repository.owner, repository.name)
+
+ redirect(s"/${repository.owner}/${repository.name}/wiki")
+ }
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
@@ -119,7 +152,7 @@
})
get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
- JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
+ using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => wiki.html.history(None, logs, repository)
case Left(_) => NotFound
@@ -128,19 +161,21 @@
})
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
- getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content =>
- contentType = "application/octet-stream"
- content
+ val path = multiParams("splat").head
+
+ getFileContent(repository.owner, repository.name, path).map { bytes =>
+ contentType = FileUtil.getContentType(path, bytes)
+ bytes
} getOrElse NotFound
})
private def unique: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String, params: Map[String, String]): Option[String] =
getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
}
private def pagename: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
@@ -150,5 +185,22 @@
}
}
+ private def conflictForNew: Constraint = new Constraint(){
+ override def validate(name: String, value: String): Option[String] = {
+ optionIf(targetWikiPage.nonEmpty){
+ Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
+ }
+ }
+ }
-}
\ No newline at end of file
+ private def conflictForEdit: Constraint = new Constraint(){
+ override def validate(name: String, value: String): Option[String] = {
+ optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
+ Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
+ }
+ }
+ }
+
+ private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
+
+}
diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala
index d0f1f59..4324d0f 100644
--- a/src/main/scala/model/Account.scala
+++ b/src/main/scala/model/Account.scala
@@ -4,6 +4,7 @@
object Accounts extends Table[Account]("ACCOUNT") {
def userName = column[String]("USER_NAME", O PrimaryKey)
+ def fullName = column[String]("FULL_NAME")
def mailAddress = column[String]("MAIL_ADDRESS")
def password = column[String]("PASSWORD")
def isAdmin = column[Boolean]("ADMINISTRATOR")
@@ -12,11 +13,13 @@
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE")
def image = column[String]("IMAGE")
- def * = userName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? <> (Account, Account.unapply _)
+ def groupAccount = column[Boolean]("GROUP_ACCOUNT")
+ def * = userName ~ fullName ~ mailAddress ~ password ~ isAdmin ~ url.? ~ registeredDate ~ updatedDate ~ lastLoginDate.? ~ image.? ~ groupAccount <> (Account, Account.unapply _)
}
case class Account(
userName: String,
+ fullName: String,
mailAddress: String,
password: String,
isAdmin: Boolean,
@@ -24,5 +27,6 @@
registeredDate: java.util.Date,
updatedDate: java.util.Date,
lastLoginDate: Option[java.util.Date],
- image: Option[String]
+ image: Option[String],
+ isGroupAccount: Boolean
)
diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala
new file mode 100644
index 0000000..0bcd0af
--- /dev/null
+++ b/src/main/scala/model/GroupMembers.scala
@@ -0,0 +1,14 @@
+package model
+
+import scala.slick.driver.H2Driver.simple._
+
+object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") {
+ def groupName = column[String]("GROUP_NAME", O PrimaryKey)
+ def userName = column[String]("USER_NAME", O PrimaryKey)
+ def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _)
+}
+
+case class GroupMember(
+ groupName: String,
+ userName: String
+)
\ No newline at end of file
diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala
index d134b8e..d5ce8a3 100644
--- a/src/main/scala/model/Issue.scala
+++ b/src/main/scala/model/Issue.scala
@@ -7,6 +7,11 @@
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
+object IssueOutline extends Table[(String, String, Int, Int)]("ISSUE_OUTLINE_VIEW") with IssueTemplate {
+ def commentCount = column[Int]("COMMENT_COUNT")
+ def * = userName ~ repositoryName ~ issueId ~ commentCount
+}
+
object Issues extends Table[Issue]("ISSUE") with IssueTemplate with MilestoneTemplate {
def openedUserName = column[String]("OPENED_USER_NAME")
def assignedUserName = column[String]("ASSIGNED_USER_NAME")
@@ -15,7 +20,8 @@
def closed = column[Boolean]("CLOSED")
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
- def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _)
+ def pullRequest = column[Boolean]("PULL_REQUEST")
+ def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _)
def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId)
}
@@ -31,4 +37,5 @@
content: Option[String],
closed: Boolean,
registeredDate: java.util.Date,
- updatedDate: java.util.Date)
\ No newline at end of file
+ updatedDate: java.util.Date,
+ isPullRequest: Boolean)
\ No newline at end of file
diff --git a/src/main/scala/model/PullRequest.scala b/src/main/scala/model/PullRequest.scala
new file mode 100644
index 0000000..0fb3205
--- /dev/null
+++ b/src/main/scala/model/PullRequest.scala
@@ -0,0 +1,28 @@
+package model
+
+import scala.slick.driver.H2Driver.simple._
+
+object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate {
+ def branch = column[String]("BRANCH")
+ def requestUserName = column[String]("REQUEST_USER_NAME")
+ def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME")
+ def requestBranch = column[String]("REQUEST_BRANCH")
+ def commitIdFrom = column[String]("COMMIT_ID_FROM")
+ def commitIdTo = column[String]("COMMIT_ID_TO")
+ def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _)
+
+ def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId)
+ def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId)
+}
+
+case class PullRequest(
+ userName: String,
+ repositoryName: String,
+ issueId: Int,
+ branch: String,
+ requestUserName: String,
+ requestRepositoryName: String,
+ requestBranch: String,
+ commitIdFrom: String,
+ commitIdTo: String
+)
\ No newline at end of file
diff --git a/src/main/scala/model/Repository.scala b/src/main/scala/model/Repository.scala
index fcfb336..215b670 100644
--- a/src/main/scala/model/Repository.scala
+++ b/src/main/scala/model/Repository.scala
@@ -9,7 +9,11 @@
def registeredDate = column[java.util.Date]("REGISTERED_DATE")
def updatedDate = column[java.util.Date]("UPDATED_DATE")
def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE")
- def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _)
+ def originUserName = column[String]("ORIGIN_USER_NAME")
+ def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
+ def parentUserName = column[String]("PARENT_USER_NAME")
+ def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
+ def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
}
@@ -22,5 +26,9 @@
defaultBranch: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date,
- lastActivityDate: java.util.Date
+ lastActivityDate: java.util.Date,
+ originUserName: Option[String],
+ originRepositoryName: Option[String],
+ parentUserName: Option[String],
+ parentRepositoryName: Option[String]
)
diff --git a/src/main/scala/model/WebHook.scala b/src/main/scala/model/WebHook.scala
new file mode 100644
index 0000000..63b0fac
--- /dev/null
+++ b/src/main/scala/model/WebHook.scala
@@ -0,0 +1,16 @@
+package model
+
+import scala.slick.driver.H2Driver.simple._
+
+object WebHooks extends Table[WebHook]("WEB_HOOK") with BasicTemplate {
+ def url = column[String]("URL")
+ def * = userName ~ repositoryName ~ url <> (WebHook, WebHook.unapply _)
+
+ def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind)
+}
+
+case class WebHook(
+ userName: String,
+ repositoryName: String,
+ url: String
+)
diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala
index 475da12..3280c35 100644
--- a/src/main/scala/model/package.scala
+++ b/src/main/scala/model/package.scala
@@ -1,5 +1,6 @@
package object model {
- import scala.slick.lifted.MappedTypeMapper
+ import scala.slick.driver.BasicDriver.Implicit._
+ import scala.slick.lifted.{Column, MappedTypeMapper}
// java.util.Date TypeMapper
implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Timestamp](
@@ -7,6 +8,10 @@
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.
*/
diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala
index 28223e1..5d8a312 100644
--- a/src/main/scala/service/AccountService.scala
+++ b/src/main/scala/service/AccountService.scala
@@ -3,9 +3,54 @@
import model._
import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession
+import service.SystemSettingsService.SystemSettings
+import util.StringUtil._
+import model.GroupMember
+import scala.Some
+import model.Account
+import util.LDAPUtil
+import org.slf4j.LoggerFactory
trait AccountService {
+ private val logger = LoggerFactory.getLogger(classOf[AccountService])
+
+ def authenticate(settings: SystemSettings, userName: String, password: String): Option[Account] =
+ if(settings.ldapAuthentication){
+ ldapAuthentication(settings, userName, password)
+ } else {
+ defaultAuthentication(userName, password)
+ }
+
+ /**
+ * Authenticate by internal database.
+ */
+ private def defaultAuthentication(userName: String, password: String) = {
+ getAccountByUserName(userName).collect {
+ case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account)
+ } getOrElse None
+ }
+
+ /**
+ * Authenticate by LDAP.
+ */
+ private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = {
+ LDAPUtil.authenticate(settings.ldap.get, userName, password) match {
+ case Right(mailAddress) => {
+ // Create or update account by LDAP information
+ getAccountByUserName(userName) match {
+ case Some(x) => updateAccount(x.copy(mailAddress = mailAddress))
+ case None => createAccount(userName, "", userName, mailAddress, false, None)
+ }
+ getAccountByUserName(userName)
+ }
+ case Left(errorMessage) => {
+ logger.info(s"LDAP Authentication Failed: ${errorMessage}")
+ defaultAuthentication(userName, password)
+ }
+ }
+ }
+
def getAccountByUserName(userName: String): Option[Account] =
Query(Accounts) filter(_.userName is userName.bind) firstOption
@@ -14,24 +59,27 @@
def getAllUsers(): List[Account] = Query(Accounts) sortBy(_.userName) list
- def createAccount(userName: String, password: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
+ def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]): Unit =
Accounts insert Account(
userName = userName,
password = password,
+ fullName = fullName,
mailAddress = mailAddress,
isAdmin = isAdmin,
url = url,
registeredDate = currentDate,
updatedDate = currentDate,
lastLoginDate = None,
- image = None)
+ image = None,
+ isGroupAccount = false)
def updateAccount(account: Account): Unit =
Accounts
.filter { a => a.userName is account.userName.bind }
- .map { a => a.password ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
+ .map { a => a.password ~ a.fullName ~ a.mailAddress ~ a.isAdmin ~ a.url.? ~ a.registeredDate ~ a.updatedDate ~ a.lastLoginDate.? }
.update (
account.password,
+ account.fullName,
account.mailAddress,
account.isAdmin,
account.url,
@@ -44,5 +92,43 @@
def updateLastLoginDate(userName: String): Unit =
Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate)
-
+
+ def createGroup(groupName: String, url: Option[String]): Unit =
+ Accounts insert Account(
+ userName = groupName,
+ password = "",
+ fullName = groupName,
+ mailAddress = groupName + "@devnull",
+ isAdmin = false,
+ url = url,
+ registeredDate = currentDate,
+ updatedDate = currentDate,
+ lastLoginDate = None,
+ image = None,
+ isGroupAccount = true)
+
+ def updateGroup(groupName: String, url: Option[String]): Unit =
+ Accounts.filter(_.userName is groupName.bind).map(_.url.?).update(url)
+
+ def updateGroupMembers(groupName: String, members: List[String]): Unit = {
+ Query(GroupMembers).filter(_.groupName is groupName.bind).delete
+ members.foreach { userName =>
+ GroupMembers insert GroupMember (groupName, userName)
+ }
+ }
+
+ def getGroupMembers(groupName: String): List[String] =
+ Query(GroupMembers)
+ .filter(_.groupName is groupName.bind)
+ .sortBy(_.userName)
+ .map(_.userName)
+ .list
+
+ def getGroupsByUserName(userName: String): List[String] =
+ Query(GroupMembers)
+ .filter(_.userName is userName.bind)
+ .sortBy(_.groupName)
+ .map(_.groupName)
+ .list
+
}
diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala
index 0b7e261..b607488 100644
--- a/src/main/scala/service/ActivityService.scala
+++ b/src/main/scala/service/ActivityService.scala
@@ -6,23 +6,23 @@
trait ActivityService {
- def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] = {
- val q = Query(Activities)
+ def getActivitiesByUser(activityUserName: String, isPublic: Boolean): List[Activity] =
+ Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
+ .filter { case (t1, t2) =>
+ if(isPublic){
+ (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind)
+ } else {
+ (t1.activityUserName is activityUserName.bind)
+ }
+ }
+ .sortBy { case (t1, t2) => t1.activityId desc }
+ .map { case (t1, t2) => t1 }
+ .take(30)
+ .list
- (if(isPublic){
- q filter { case (t1, t2) => (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) }
- } else {
- q filter { case (t1, t2) => t1.activityUserName is activityUserName.bind }
- })
- .sortBy { case (t1, t2) => t1.activityId desc }
- .map { case (t1, t2) => t1 }
- .take(30)
- .list
- }
-
def getRecentActivities(): List[Activity] =
- Query(Activities)
+ Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
.filter { case (t1, t2) => t2.isPrivate is false.bind }
.sortBy { case (t1, t2) => t1.activityId desc }
@@ -52,6 +52,13 @@
Some(title),
currentDate)
+ def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
+ Activities.autoInc insert(userName, repositoryName, activityUserName,
+ "close_issue",
+ s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
+ Some(title),
+ currentDate)
+
def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"reopen_issue",
@@ -65,7 +72,14 @@
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
Some(cut(comment, 200)),
currentDate)
-
+
+ def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String): Unit =
+ Activities.autoInc insert(userName, repositoryName, activityUserName,
+ "comment_issue",
+ s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
+ Some(cut(comment, 200)),
+ currentDate)
+
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"create_wiki",
@@ -73,11 +87,11 @@
Some(pageName),
currentDate)
- def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) =
+ def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
"edit_wiki",
s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki",
- Some(pageName),
+ Some(pageName + ":" + commitId),
currentDate)
def recordPushActivity(userName: String, repositoryName: String, activityUserName: String,
@@ -98,15 +112,42 @@
def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) =
Activities.autoInc insert(userName, repositoryName, activityUserName,
- "create_tag",
+ "create_branch",
s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
None,
currentDate)
-
+
+ def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) =
+ Activities.autoInc insert(userName, repositoryName, activityUserName,
+ "fork",
+ s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
+ None,
+ currentDate)
+
+ def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit =
+ Activities.autoInc insert(userName, repositoryName, activityUserName,
+ "open_pullreq",
+ s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
+ Some(title),
+ currentDate)
+
+ def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit =
+ Activities.autoInc insert(userName, repositoryName, activityUserName,
+ "merge_pullreq",
+ s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
+ Some(message),
+ currentDate)
+
def insertCommitId(userName: String, repositoryName: String, commitId: String) = {
CommitLog insert (userName, repositoryName, commitId)
}
-
+
+ def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) =
+ CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*)
+
+ def getAllCommitIds(userName: String, repositoryName: String): List[String] =
+ Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list
+
def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean =
Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index aef6436..16fa972 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -6,8 +6,8 @@
import Q.interpolation
import model._
-import util.StringUtil._
import util.Implicits._
+import util.StringUtil._
trait IssuesService {
import IssuesService._
@@ -42,33 +42,28 @@
/**
* Returns the count of the search result against issues.
*
- * @param owner the repository owner
- * @param repository the repository name
* @param condition the search condition
- * @param filter the filter type ("all", "assigned" or "created_by")
- * @param userName the filter user name required for "assigned" and "created_by"
+ * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
+ * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
+ * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result
*/
- def countIssue(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]): Int = {
- // TODO It must be _.length instead of map (_.issueId) list).length.
- // But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
- // https://github.com/slick/slick/issues/170
- (searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
- }
+ def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
+ repos: (String, String)*): Int =
+ Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
/**
* Returns the Map which contains issue count for each labels.
*
* @param owner the repository owner
* @param repository the repository name
* @param condition the search condition
- * @param filter the filter type ("all", "assigned" or "created_by")
- * @param userName the filter user name required for "assigned" and "created_by"
- * @return the Map which contains issue count for each labels (key is label name, value is issue count),
+ * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
+ * @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
- filter: String, userName: Option[String]): Map[String, Int] = {
+ filterUser: Map[String, String]): Map[String, Int] = {
- searchIssueQuery(owner, repository, condition.copy(labels = Set.empty), filter, userName)
+ searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
.innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
}
@@ -83,76 +78,100 @@
}
.toMap
}
+ /**
+ * Returns list which contains issue count for each repository.
+ * If the issue does not exist, its repository is not included in the result.
+ *
+ * @param condition the search condition
+ * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
+ * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
+ * @param repos Tuple of the repository owner and the repository name
+ * @return list which contains issue count for each repository
+ */
+ def countIssueGroupByRepository(
+ condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
+ repos: (String, String)*): List[(String, String, Int)] = {
+ searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
+ .groupBy { t =>
+ t.userName ~ t.repositoryName
+ }
+ .map { case (repo, t) =>
+ repo ~ t.length
+ }
+ .sortBy(_._3 desc)
+ .list
+ }
/**
* Returns the search result against issues.
*
- * @param owner the repository owner
- * @param repository the repository name
* @param condition the search condition
- * @param filter the filter type ("all", "assigned" or "created_by")
- * @param userName the filter user name required for "assigned" and "created_by"
+ * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name)
+ * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param offset the offset for pagination
* @param limit the limit for pagination
+ * @param repos Tuple of the repository owner and the repository name
* @return the search result (list of tuples which contain issue, labels and comment count)
*/
- def searchIssue(owner: String, repository: String, condition: IssueSearchCondition,
- filter: String, userName: Option[String], offset: Int, limit: Int): List[(Issue, List[Label], Int)] = {
+ def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
+ offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = {
- // get issues and comment count
- val issues = searchIssueQuery(owner, repository, condition, filter, userName)
- .leftJoin(Query(IssueComments)
- .filter { t =>
- (t.byRepository(owner, repository)) &&
- (t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
+ // get issues and comment count and labels
+ searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
+ .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
+ .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
+ .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
+ .map { case (((t1, t2), t3), t4) =>
+ (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
}
- .groupBy { _.issueId }
- .map { case (issueId, t) => issueId ~ t.length }).on((t1, t2) => t1.issueId is t2._1)
- .sortBy { case (t1, t2) =>
- (condition.sort match {
- case "created" => t1.registeredDate
- case "comments" => t2._2
- case "updated" => t1.updatedDate
- }) match {
- case sort => condition.direction match {
- case "asc" => sort asc
- case "desc" => sort desc
+ .sortBy(_._4) // labelName
+ .sortBy { case (t1, commentCount, _,_,_) =>
+ (condition.sort match {
+ case "created" => t1.registeredDate
+ case "comments" => commentCount
+ case "updated" => t1.updatedDate
+ }) match {
+ case sort => condition.direction match {
+ case "asc" => sort asc
+ case "desc" => sort desc
+ }
}
}
- }
- .map { case (t1, t2) => (t1, t2._2.ifNull(0)) }
- .drop(offset).take(limit)
- .list
-
- // get labels
- val labels = Query(IssueLabels)
- .innerJoin(Labels).on { (t1, t2) =>
- t1.byLabel(t2.userName, t2.repositoryName, t2.labelId)
- }
- .filter { case (t1, t2) =>
- (t1.byRepository(owner, repository)) &&
- (t1.issueId inSetBind (issues.map(_._1.issueId)))
- }
- .sortBy { case (t1, t2) => t1.issueId ~ t2.labelName }
- .map { case (t1, t2) => (t1.issueId, t2) }
- .list
-
- issues.map { case (issue, commentCount) =>
- (issue, labels.collect { case (issueId, labels) if(issueId == issue.issueId) => labels }, commentCount)
- }
+ .drop(offset).take(limit)
+ .list
+ .splitWith { (c1, c2) =>
+ c1._1.userName == c2._1.userName &&
+ c1._1.repositoryName == c2._1.repositoryName &&
+ c1._1.issueId == c2._1.issueId
+ }
+ .map { issues => issues.head match {
+ case (issue, commentCount, _,_,_) =>
+ (issue,
+ issues.flatMap { t => t._3.map (
+ Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
+ )} toList,
+ commentCount)
+ }} toList
}
/**
* Assembles query for conditional issue searching.
*/
- private def searchIssueQuery(owner: String, repository: String, condition: IssueSearchCondition, filter: String, userName: Option[String]) =
+ private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
+ filterUser: Map[String, String], onlyPullRequest: Boolean) =
Query(Issues) filter { t1 =>
- (t1.byRepository(owner, repository)) &&
+ condition.repo
+ .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
+ .getOrElse (repos)
+ .map { case (owner, repository) => t1.byRepository(owner, repository) }
+ .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed is (condition.state == "closed").bind) &&
(t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId isNull, condition.milestoneId == Some(None)) &&
- (t1.assignedUserName is userName.get.bind, filter == "assigned") &&
- (t1.openedUserName is userName.get.bind, filter == "created_by") &&
+ (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
+ (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
+ (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
+ (t1.pullRequest is true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -164,7 +183,7 @@
}
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
- assignedUserName: Option[String], milestoneId: Option[Int]) =
+ assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) =
// next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
.firstOption.filter { id =>
@@ -179,7 +198,8 @@
content,
false,
currentDate,
- currentDate)
+ currentDate,
+ isPullRequest)
// increment issue id
IssueId
@@ -237,6 +257,60 @@
}
.update (closed, currentDate)
+ /**
+ * Search issues by keyword.
+ *
+ * @param owner the repository owner
+ * @param repository the repository name
+ * @param query the keywords separated by whitespace.
+ * @return issues with comment count and matched content of issue or comment
+ */
+ def searchIssuesByKeyword(owner: String, repository: String, query: String): List[(Issue, Int, String)] = {
+ import scala.slick.driver.H2Driver.likeEncode
+ val keywords = splitWords(query.toLowerCase)
+
+ // Search Issue
+ val issues = Issues
+ .innerJoin(IssueOutline).on { case (t1, t2) =>
+ t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
+ }
+ .filter { case (t1, t2) =>
+ keywords.map { keyword =>
+ (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
+ (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
+ } .reduceLeft(_ && _)
+ }
+ .map { case (t1, t2) =>
+ (t1, 0, t1.content.?, t2.commentCount)
+ }
+
+ // Search IssueComment
+ val comments = IssueComments
+ .innerJoin(Issues).on { case (t1, t2) =>
+ t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
+ }
+ .innerJoin(IssueOutline).on { case ((t1, t2), t3) =>
+ t2.byIssue(t3.userName, t3.repositoryName, t3.issueId)
+ }
+ .filter { case ((t1, t2), t3) =>
+ keywords.map { query =>
+ t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
+ }.reduceLeft(_ && _)
+ }
+ .map { case ((t1, t2), t3) =>
+ (t2, t1.commentId, t1.content.?, t3.commentCount)
+ }
+
+ issues.union(comments).sortBy { case (issue, commentId, _, _) =>
+ issue.issueId ~ commentId
+ }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
+ issue1.issueId == issue2.issueId
+ }.map { _.head match {
+ case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
+ }
+ }.toList
+ }
+
}
object IssuesService {
@@ -247,6 +321,7 @@
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None,
+ repo: Option[String] = None,
state: String = "open",
sort: String = "created",
direction: String = "desc"){
@@ -258,6 +333,7 @@
case Some(x) => x.toString
case None => "none"
})},
+ repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)),
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
@@ -274,12 +350,21 @@
def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition(
param(request, "labels").map(_.split(" ").toSet).getOrElse(Set.empty),
- param(request, "milestone").map(_ match {
+ param(request, "milestone").map{
case "none" => None
- case x => Some(x.toInt)
- }),
+ case x => x.toIntOpt
+ },
+ param(request, "for"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"))
+
+ def page(request: HttpServletRequest) = try {
+ val i = param(request, "page").getOrElse("1").toInt
+ if(i <= 0) 1 else i
+ } catch {
+ case e: NumberFormatException => 1
+ }
}
+
}
diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala
new file mode 100644
index 0000000..5374b99
--- /dev/null
+++ b/src/main/scala/service/PullRequestService.scala
@@ -0,0 +1,54 @@
+package service
+
+import scala.slick.driver.H2Driver.simple._
+import Database.threadLocalSession
+import model._
+import util.ControlUtil._
+
+trait PullRequestService { self: IssuesService =>
+ import PullRequestService._
+
+ def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] =
+ getIssue(owner, repository, issueId.toString).flatMap{ issue =>
+ Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{
+ pullreq => (issue, pullreq)
+ }
+ }
+
+ def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] =
+ Query(PullRequests)
+ .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
+ .filter { case (t1, t2) =>
+ (t2.closed is closed.bind) &&
+ (t1.userName is owner.bind) &&
+ (t1.repositoryName is repository.get.bind, repository.isDefined)
+ }
+ .groupBy { case (t1, t2) => t2.openedUserName }
+ .map { case (userName, t) => userName ~ t.length }
+ .sortBy(_._2 desc)
+ .list
+ .map { x => PullRequestCount(x._1, x._2) }
+
+ def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
+ originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
+ commitIdFrom: String, commitIdTo: String): Unit =
+ PullRequests insert (PullRequest(
+ originUserName,
+ originRepositoryName,
+ issueId,
+ originBranch,
+ requestUserName,
+ requestRepositoryName,
+ requestBranch,
+ commitIdFrom,
+ commitIdTo))
+
+}
+
+object PullRequestService {
+
+ val PullRequestLimit = 25
+
+ case class PullRequestCount(userName: String, count: Int)
+
+}
diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala
new file mode 100644
index 0000000..ac4f177
--- /dev/null
+++ b/src/main/scala/service/RepositorySearchService.scala
@@ -0,0 +1,126 @@
+package service
+
+import util.{FileUtil, StringUtil, JGitUtil}
+import util.Directory._
+import util.ControlUtil._
+import model.Issue
+import org.eclipse.jgit.revwalk.RevWalk
+import org.eclipse.jgit.treewalk.TreeWalk
+import scala.collection.mutable.ListBuffer
+import org.eclipse.jgit.lib.FileMode
+import org.eclipse.jgit.api.Git
+
+trait
+RepositorySearchService { self: IssuesService =>
+ import RepositorySearchService._
+
+ def countIssues(owner: String, repository: String, query: String): Int =
+ searchIssuesByKeyword(owner, repository, query).length
+
+ def searchIssues(owner: String, repository: String, query: String): List[IssueSearchResult] =
+ searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
+ IssueSearchResult(
+ issue.issueId,
+ issue.title,
+ issue.openedUserName,
+ issue.registeredDate,
+ commentCount,
+ getHighlightText(content, query)._1)
+ }
+
+ def countFiles(owner: String, repository: String, query: String): Int =
+ using(Git.open(getRepositoryDir(owner, repository))){ git =>
+ if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
+ }
+
+ def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] =
+ using(Git.open(getRepositoryDir(owner, repository))){ git =>
+ if(JGitUtil.isEmpty(git)){
+ Nil
+ } else {
+ val files = searchRepositoryFiles(git, query)
+ val commits = JGitUtil.getLatestCommitFromPaths(git, files.toList.map(_._1), "HEAD")
+ files.map { case (path, text) =>
+ val (highlightText, lineNumber) = getHighlightText(text, query)
+ FileSearchResult(
+ path,
+ commits(path).getCommitterIdent.getWhen,
+ highlightText,
+ lineNumber)
+ }
+ }
+ }
+
+ private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
+ val revWalk = new RevWalk(git.getRepository)
+ val objectId = git.getRepository.resolve("HEAD")
+ val revCommit = revWalk.parseCommit(objectId)
+ val treeWalk = new TreeWalk(git.getRepository)
+ treeWalk.setRecursive(true)
+ treeWalk.addTree(revCommit.getTree)
+
+ val keywords = StringUtil.splitWords(query.toLowerCase)
+ val list = new ListBuffer[(String, String)]
+
+ while (treeWalk.next()) {
+ if(treeWalk.getFileMode(0) != FileMode.TREE){
+ JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes =>
+ if(FileUtil.isText(bytes)){
+ val text = StringUtil.convertFromByteArray(bytes)
+ val lowerText = text.toLowerCase
+ val indices = keywords.map(lowerText.indexOf _)
+ if(!indices.exists(_ < 0)){
+ list.append((treeWalk.getPathString, text))
+ }
+ }
+ }
+ }
+ }
+ treeWalk.release
+ revWalk.release
+
+ list.toList
+ }
+
+}
+
+object RepositorySearchService {
+
+ val CodeLimit = 10
+ val IssueLimit = 10
+
+ def getHighlightText(content: String, query: String): (String, Int) = {
+ val keywords = StringUtil.splitWords(query.toLowerCase)
+ val lowerText = content.toLowerCase
+ val indices = keywords.map(lowerText.indexOf _)
+
+ if(!indices.exists(_ < 0)){
+ val lineNumber = content.substring(0, indices.min).split("\n").size - 1
+ val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n"))
+ .replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")",
+ "$1")
+ (highlightText, lineNumber + 1)
+ } else {
+ (content.split("\n").take(5).mkString("\n"), 1)
+ }
+ }
+
+ case class SearchResult(
+ files : List[(String, String)],
+ issues: List[(Issue, Int, String)])
+
+ case class IssueSearchResult(
+ issueId: Int,
+ title: String,
+ openedUserName: String,
+ registeredDate: java.util.Date,
+ commentCount: Int,
+ highlightText: String)
+
+ case class FileSearchResult(
+ path: String,
+ lastModified: java.util.Date,
+ highlightText: String,
+ highlightLineNumber: Int)
+
+}
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index 153aa95..a443b81 100644
--- a/src/main/scala/service/RepositoryService.scala
+++ b/src/main/scala/service/RepositoryService.scala
@@ -15,19 +15,27 @@
* @param userName the user name of the repository owner
* @param description the repository description
* @param isPrivate the repository type (private is true, otherwise false)
+ * @param originRepositoryName specify for the forked repository. (default is None)
+ * @param originUserName specify for the forked repository. (default is None)
*/
- def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = {
+ def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
+ originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
+ parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = {
Repositories insert
Repository(
- userName = userName,
- repositoryName = repositoryName,
- isPrivate = isPrivate,
- description = description,
- defaultBranch = "master",
- registeredDate = currentDate,
- updatedDate = currentDate,
- lastActivityDate = currentDate)
-
+ userName = userName,
+ repositoryName = repositoryName,
+ isPrivate = isPrivate,
+ description = description,
+ defaultBranch = "master",
+ registeredDate = currentDate,
+ updatedDate = currentDate,
+ lastActivityDate = currentDate,
+ originUserName = originUserName,
+ originRepositoryName = originRepositoryName,
+ parentUserName = parentUserName,
+ parentRepositoryName = parentRepositoryName)
+
IssueId insert (userName, repositoryName, 0)
}
@@ -39,8 +47,10 @@
Labels .filter(_.byRepository(userName, repositoryName)).delete
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
Issues .filter(_.byRepository(userName, repositoryName)).delete
+ PullRequests .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
+ WebHooks .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
}
@@ -54,39 +64,6 @@
Query(Repositories) filter(_.userName is userName.bind) map (_.repositoryName) list
/**
- * Returns the list of specified user's repositories information.
- *
- * @param userName the user name
- * @param baseUrl the base url of this application
- * @param loginUserName the logged in user name
- * @return the list of repository information which is sorted in descending order of lastActivityDate.
- */
- def getVisibleRepositories(userName: String, baseUrl: String, loginUserName: Option[String]): List[RepositoryInfo] = {
- val q1 = Repositories
- .filter { t => t.userName is userName.bind }
- .map { r => r }
-
- val q2 = Collaborators
- .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
- .filter{ case (t1, t2) => t1.collaboratorName is userName.bind}
- .map { case (t1, t2) => t2 }
-
- def visibleFor(t: Repositories.type, loginUserName: Option[String]) = {
- loginUserName match {
- case Some(x) => (t.isPrivate is false.bind) || (
- (t.isPrivate is true.bind) && ((t.userName is x.bind) || (Collaborators.filter { c =>
- c.byRepository(t.userName, t.repositoryName) && (c.collaboratorName is x.bind)
- }.exists)))
- case None => (t.isPrivate is false.bind)
- }
- }
-
- q1.union(q2).filter(visibleFor(_, loginUserName)).sortBy(_.lastActivityDate desc).list map { repository =>
- new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
- }
- }
-
- /**
* Returns the specified repository information.
*
* @param userName the user name of the repository owner
@@ -96,34 +73,69 @@
*/
def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = {
(Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
- new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
+ // for getting issue count and pull request count
+ val issues = Query(Issues).filter { t =>
+ t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind)
+ }.map(_.pullRequest).list
+
+ new RepositoryInfo(
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ repository,
+ issues.size,
+ issues.filter(_ == true).size,
+ getForkedCount(
+ repository.originUserName.getOrElse(repository.userName),
+ repository.originRepositoryName.getOrElse(repository.repositoryName)
+ ))
+ }
+ }
+
+ def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = {
+ Query(Repositories).filter { t1 =>
+ (t1.userName is userName.bind) ||
+ (Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists)
+ }.sortBy(_.lastActivityDate desc).list.map{ repository =>
+ new RepositoryInfo(
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ repository,
+ getForkedCount(
+ repository.originUserName.getOrElse(repository.userName),
+ repository.originRepositoryName.getOrElse(repository.repositoryName)
+ ))
}
}
/**
- * Returns the list of accessible repositories information for the specified account user.
- *
- * @param account the account
+ * Returns the list of visible repositories for the specified user.
+ * If repositoryUserName is given then filters results by repository owner.
+ *
+ * @param loginAccount the logged in account
* @param baseUrl the base url of this application
- * @return the repository informations which is sorted in descending order of lastActivityDate.
+ * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
+ * @return the repository information which is sorted in descending order of lastActivityDate.
*/
- def getAccessibleRepositories(account: Option[Account], baseUrl: String): List[RepositoryInfo] = {
-
- def newRepositoryInfo(repository: Repository): RepositoryInfo = {
- new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository)
- }
-
- (account match {
+ def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None): List[RepositoryInfo] = {
+ (loginAccount match {
// for Administrators
case Some(x) if(x.isAdmin) => Query(Repositories)
// for Normal Users
case Some(x) if(!x.isAdmin) =>
Query(Repositories) filter { t => (t.isPrivate is false.bind) ||
- (Query(Collaborators).filter(t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)) exists)
+ (Query(Collaborators).filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists)
}
// for Guests
case None => Query(Repositories) filter(_.isPrivate is false.bind)
- }).sortBy(_.lastActivityDate desc).list.map(newRepositoryInfo _)
+ }).filter { t =>
+ repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE
+ }.sortBy(_.lastActivityDate desc).list.map{ repository =>
+ new RepositoryInfo(
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ repository,
+ getForkedCount(
+ repository.originUserName.getOrElse(repository.userName),
+ repository.originRepositoryName.getOrElse(repository.repositoryName)
+ ))
+ }
}
/**
@@ -162,6 +174,15 @@
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/**
+ * Remove all collaborators from the repository.
+ *
+ * @param userName the user name of the repository owner
+ * @param repositoryName the repository name
+ */
+ def removeCollaborators(userName: String, repositoryName: String): Unit =
+ Collaborators.filter(_.byRepository(userName, repositoryName)).delete
+
+ /**
* Returns the list of collaborators name which is sorted with ascending order.
*
* @param userName the user name of the repository owner
@@ -180,17 +201,39 @@
}
}
+ private def getForkedCount(userName: String, repositoryName: String): Int =
+ Query(Repositories.filter { t =>
+ (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
+ }.length).first
+
+
+ def getForkedRepositories(userName: String, repositoryName: String): List[String] =
+ Query(Repositories).filter { t =>
+ (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
+ }
+ .sortBy(_.userName asc).map(_.userName).list
+
}
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
- commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
+ issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
+ branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
- def this(repo: JGitUtil.RepositoryInfo, model: Repository) = {
- this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags)
- }
+ /**
+ * Creates instance with issue count and pull request count.
+ */
+ def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) =
+ this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags)
+ /**
+ * Creates instance without issue count and pull request count.
+ */
+ def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) =
+ this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags)
}
+ case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
+
}
\ No newline at end of file
diff --git a/src/main/scala/service/RequestCache.scala b/src/main/scala/service/RequestCache.scala
index 059e91c..0f0b0f0 100644
--- a/src/main/scala/service/RequestCache.scala
+++ b/src/main/scala/service/RequestCache.scala
@@ -1,6 +1,7 @@
package service
import model._
+import service.SystemSettingsService.SystemSettings
/**
* This service is used for a view helper mainly.
@@ -10,6 +11,11 @@
*/
trait RequestCache {
+ def getSystemSettings()(implicit context: app.Context): SystemSettings =
+ context.cache("system_settings"){
+ new SystemSettingsService {}.loadSystemSettings()
+ }
+
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: app.Context): Option[Issue] = {
context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){
new IssuesService {}.getIssue(userName, repositoryName, issueId)
diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala
index b425b34..5ed9eb3 100644
--- a/src/main/scala/service/SystemSettingsService.scala
+++ b/src/main/scala/service/SystemSettingsService.scala
@@ -1,40 +1,152 @@
-package service
-
-import util.Directory._
-import SystemSettingsService._
-
-trait SystemSettingsService {
-
- def saveSystemSettings(settings: SystemSettings): Unit = {
- val props = new java.util.Properties()
- props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
- props.store(new java.io.FileOutputStream(GitBucketConf), null)
- }
-
-
- def loadSystemSettings(): SystemSettings = {
- val props = new java.util.Properties()
- if(GitBucketConf.exists){
- props.load(new java.io.FileInputStream(GitBucketConf))
- }
- SystemSettings(getBoolean(props, "allow_account_registration"))
- }
-
-}
-
-object SystemSettingsService {
-
- case class SystemSettings(allowAccountRegistration: Boolean)
-
- private val AllowAccountRegistration = "allow_account_registration"
-
- private def getBoolean(props: java.util.Properties, key: String, default: Boolean = false): Boolean = {
- val value = props.getProperty(key)
- if(value == null || value.isEmpty){
- default
- } else {
- value.toBoolean
- }
- }
-
-}
+package service
+
+import util.Directory._
+import util.ControlUtil._
+import SystemSettingsService._
+
+trait SystemSettingsService {
+
+ def saveSystemSettings(settings: SystemSettings): Unit = {
+ defining(new java.util.Properties()){ props =>
+ props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
+ props.setProperty(Gravatar, settings.gravatar.toString)
+ props.setProperty(Notification, settings.notification.toString)
+ if(settings.notification) {
+ settings.smtp.foreach { smtp =>
+ props.setProperty(SmtpHost, smtp.host)
+ smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
+ smtp.user.foreach(props.setProperty(SmtpUser, _))
+ smtp.password.foreach(props.setProperty(SmtpPassword, _))
+ smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
+ smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
+ smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
+ }
+ }
+ props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
+ if(settings.ldapAuthentication){
+ settings.ldap.map { ldap =>
+ props.setProperty(LdapHost, ldap.host)
+ ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
+ ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
+ ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
+ props.setProperty(LdapBaseDN, ldap.baseDN)
+ props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
+ props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute)
+ }
+ }
+ props.store(new java.io.FileOutputStream(GitBucketConf), null)
+ }
+ }
+
+
+ def loadSystemSettings(): SystemSettings = {
+ defining(new java.util.Properties()){ props =>
+ if(GitBucketConf.exists){
+ props.load(new java.io.FileInputStream(GitBucketConf))
+ }
+ SystemSettings(
+ getValue(props, AllowAccountRegistration, false),
+ getValue(props, Gravatar, true),
+ getValue(props, Notification, false),
+ if(getValue(props, Notification, false)){
+ Some(Smtp(
+ getValue(props, SmtpHost, ""),
+ getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
+ getOptionValue(props, SmtpUser, None),
+ getOptionValue(props, SmtpPassword, None),
+ getOptionValue[Boolean](props, SmtpSsl, None),
+ getOptionValue(props, SmtpFromAddress, None),
+ getOptionValue(props, SmtpFromName, None)))
+ } else {
+ None
+ },
+ getValue(props, LdapAuthentication, false),
+ if(getValue(props, LdapAuthentication, false)){
+ Some(Ldap(
+ getValue(props, LdapHost, ""),
+ getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
+ getOptionValue(props, LdapBindDN, None),
+ getOptionValue(props, LdapBindPassword, None),
+ getValue(props, LdapBaseDN, ""),
+ getValue(props, LdapUserNameAttribute, ""),
+ getValue(props, LdapMailAddressAttribute, "")))
+ } else {
+ None
+ }
+ )
+ }
+ }
+
+}
+
+object SystemSettingsService {
+ import scala.reflect.ClassTag
+
+ case class SystemSettings(
+ allowAccountRegistration: Boolean,
+ gravatar: Boolean,
+ notification: Boolean,
+ smtp: Option[Smtp],
+ ldapAuthentication: Boolean,
+ ldap: Option[Ldap])
+
+ case class Ldap(
+ host: String,
+ port: Option[Int],
+ bindDN: Option[String],
+ bindPassword: Option[String],
+ baseDN: String,
+ userNameAttribute: String,
+ mailAttribute: String)
+
+ case class Smtp(
+ host: String,
+ port: Option[Int],
+ user: Option[String],
+ password: Option[String],
+ ssl: Option[Boolean],
+ fromAddress: Option[String],
+ fromName: Option[String])
+
+ val DefaultSmtpPort = 25
+ val DefaultLdapPort = 389
+
+ private val AllowAccountRegistration = "allow_account_registration"
+ private val Gravatar = "gravatar"
+ private val Notification = "notification"
+ private val SmtpHost = "smtp.host"
+ private val SmtpPort = "smtp.port"
+ private val SmtpUser = "smtp.user"
+ private val SmtpPassword = "smtp.password"
+ private val SmtpSsl = "smtp.ssl"
+ private val SmtpFromAddress = "smtp.from_address"
+ private val SmtpFromName = "smtp.from_name"
+ private val LdapAuthentication = "ldap_authentication"
+ private val LdapHost = "ldap.host"
+ private val LdapPort = "ldap.port"
+ private val LdapBindDN = "ldap.bindDN"
+ private val LdapBindPassword = "ldap.bind_password"
+ private val LdapBaseDN = "ldap.baseDN"
+ private val LdapUserNameAttribute = "ldap.username_attribute"
+ private val LdapMailAddressAttribute = "ldap.mail_attribute"
+
+ private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty) default
+ else convertType(value).asInstanceOf[A]
+ }
+
+ private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty) default
+ else Some(convertType(value)).asInstanceOf[Option[A]]
+ }
+
+ private def convertType[A: ClassTag](value: String) =
+ defining(implicitly[ClassTag[A]].runtimeClass){ c =>
+ if(c == classOf[Boolean]) value.toBoolean
+ else if(c == classOf[Int]) value.toInt
+ else value
+ }
+
+}
diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala
new file mode 100644
index 0000000..7ddaa11
--- /dev/null
+++ b/src/main/scala/service/WebHookService.scala
@@ -0,0 +1,143 @@
+package service
+
+import scala.slick.driver.H2Driver.simple._
+import Database.threadLocalSession
+
+import model._
+import org.slf4j.LoggerFactory
+import service.RepositoryService.RepositoryInfo
+import util.JGitUtil
+import org.eclipse.jgit.diff.DiffEntry
+import util.JGitUtil.CommitInfo
+import org.eclipse.jgit.api.Git
+import org.apache.http.message.BasicNameValuePair
+import org.apache.http.client.entity.UrlEncodedFormEntity
+import org.apache.http.protocol.HTTP
+import org.apache.http.NameValuePair
+
+trait WebHookService {
+ import WebHookService._
+
+ private val logger = LoggerFactory.getLogger(classOf[WebHookService])
+
+ def getWebHookURLs(owner: String, repository: String): List[WebHook] =
+ Query(WebHooks).filter(_.byRepository(owner, repository)).sortBy(_.url).list
+
+ def addWebHookURL(owner: String, repository: String, url :String): Unit =
+ WebHooks.insert(WebHook(owner, repository, url))
+
+ def deleteWebHookURL(owner: String, repository: String, url :String): Unit =
+ Query(WebHooks).filter(_.byPrimaryKey(owner, repository, url)).delete
+
+ def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
+ import org.json4s._
+ import org.json4s.jackson.Serialization
+ import org.json4s.jackson.Serialization.{read, write}
+ import org.apache.http.client.methods.HttpPost
+ import org.apache.http.impl.client.HttpClientBuilder
+ import scala.concurrent._
+ import ExecutionContext.Implicits.global
+
+ logger.debug("start callWebHook")
+ implicit val formats = Serialization.formats(NoTypeHints)
+
+ if(webHookURLs.nonEmpty){
+ val json = write(payload)
+ val httpClient = HttpClientBuilder.create.build
+
+ webHookURLs.foreach { webHookUrl =>
+ val f = future {
+ logger.debug(s"start web hook invocation for ${webHookUrl}")
+ val httpPost = new HttpPost(webHookUrl.url)
+
+ val params: java.util.List[NameValuePair] = new java.util.ArrayList()
+ params.add(new BasicNameValuePair("payload", json))
+ httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
+
+ httpClient.execute(httpPost)
+ httpPost.releaseConnection()
+ logger.debug(s"end web hook invocation for ${webHookUrl}")
+ }
+ f.onSuccess {
+ case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
+ }
+ f.onFailure {
+ case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
+ }
+ }
+ }
+ logger.debug("end callWebHook")
+ }
+
+}
+
+object WebHookService {
+
+ case class WebHookPayload(
+ ref: String,
+ commits: List[WebHookCommit],
+ repository: WebHookRepository)
+
+ object WebHookPayload {
+ def apply(git: Git, refName: String, repositoryInfo: RepositoryInfo,
+ commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
+ WebHookPayload(
+ refName,
+ commits.map { commit =>
+ val diffs = JGitUtil.getDiffs(git, commit.id, false)
+ val commitUrl = repositoryInfo.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/commit/" + commit.id
+
+ WebHookCommit(
+ id = commit.id,
+ message = commit.fullMessage,
+ timestamp = commit.time.toString,
+ url = commitUrl,
+ added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
+ removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
+ modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
+ x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
+ author = WebHookUser(
+ name = commit.committer,
+ email = commit.mailAddress
+ )
+ )
+ }.toList,
+ WebHookRepository(
+ name = repositoryInfo.name,
+ url = repositoryInfo.url,
+ description = repositoryInfo.repository.description.getOrElse(""),
+ watchers = 0,
+ forks = repositoryInfo.forkedCount,
+ `private` = repositoryInfo.repository.isPrivate,
+ owner = WebHookUser(
+ name = repositoryOwner.userName,
+ email = repositoryOwner.mailAddress
+ )
+ )
+ )
+ }
+
+ case class WebHookCommit(
+ id: String,
+ message: String,
+ timestamp: String,
+ url: String,
+ added: List[String],
+ removed: List[String],
+ modified: List[String],
+ author: WebHookUser)
+
+ case class WebHookRepository(
+ name: String,
+ url: String,
+ description: String,
+ watchers: Int,
+ forks: Int,
+ `private`: Boolean,
+ owner: WebHookUser)
+
+ case class WebHookUser(
+ name: String,
+ email: String)
+
+}
diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala
index 8c02515..bce71e4 100644
--- a/src/main/scala/service/WikiService.scala
+++ b/src/main/scala/service/WikiService.scala
@@ -4,11 +4,11 @@
import java.util.Date
import org.eclipse.jgit.api.Git
import org.apache.commons.io.FileUtils
-import util.JGitUtil.DiffInfo
-import util.{Directory, JGitUtil}
-import org.eclipse.jgit.lib.RepositoryBuilder
+import util.{StringUtil, Directory, JGitUtil, LockUtil}
+import util.ControlUtil._
import org.eclipse.jgit.treewalk.CanonicalTreeParser
-import java.util.concurrent.ConcurrentHashMap
+import org.eclipse.jgit.diff.DiffFormatter
+import org.eclipse.jgit.api.errors.PatchApplyException
object WikiService {
@@ -19,8 +19,9 @@
* @param content the page content
* @param committer the last committer
* @param time the last modified time
+ * @param id the latest commit id
*/
- case class WikiPageInfo(name: String, content: String, committer: String, time: Date)
+ case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
/**
* The model for wiki page history.
@@ -32,65 +33,35 @@
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
- /**
- * lock objects
- */
- private val locks = new ConcurrentHashMap[String, AnyRef]()
-
- /**
- * Returns the lock object for the specified repository.
- */
- private def getLockObject(owner: String, repository: String): AnyRef = synchronized {
- val key = owner + "/" + repository
- if(!locks.containsKey(key)){
- locks.put(key, new AnyRef())
- }
- locks.get(key)
- }
-
- /**
- * Synchronizes a given function which modifies the working copy of the wiki repository.
- *
- * @param owner the repository owner
- * @param repository the repository name
- * @param f the function which modifies the working copy of the wiki repository
- * @tparam T the return type of the given function
- * @return the result of the given function
- */
- def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f)
-
}
trait WikiService {
import WikiService._
- def createWikiRepository(owner: model.Account, repository: String): Unit = {
- lock(owner.userName, repository){
- val dir = Directory.getWikiRepositoryDir(owner.userName, repository)
- if(!dir.exists){
- try {
- JGitUtil.initRepository(dir)
- saveWikiPage(owner.userName, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", owner, "Initial Commit")
- } finally {
- // once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
- FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner.userName, repository))
+ def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit =
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
+ defining(Directory.getWikiRepositoryDir(owner, repository)){ dir =>
+ if(!dir.exists){
+ try {
+ JGitUtil.initRepository(dir)
+ saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
+ } finally {
+ // once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
+ FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
+ }
}
}
}
- }
-
+
/**
* Returns the wiki page.
*/
def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = {
- JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
- try {
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
+ optionIf(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
- WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time)
+ WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
}
- } catch {
- // TODO no commit, but it should not judge by exception.
- case e: NullPointerException => None
}
}
}
@@ -98,9 +69,9 @@
/**
* Returns the content of the specified file.
*/
- def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = {
- JGitUtil.withGit(Directory.getWikiRepositoryDir(owner, repository)){ git =>
- try {
+ def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
+ optionIf(!JGitUtil.isEmpty(git)){
val index = path.lastIndexOf('/')
val parentPath = if(index < 0) "." else path.substring(0, index)
val fileName = if(index < 0) path else path.substring(index + 1)
@@ -108,57 +79,119 @@
JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
git.getRepository.open(file.id).getBytes
}
- } catch {
- // TODO no commit, but it should not judge by exception.
- case e: NullPointerException => None
}
}
- }
/**
* Returns the list of wiki page names.
*/
def getWikiPageList(owner: String, repository: String): List[String] = {
- JGitUtil.getFileList(Git.open(Directory.getWikiRepositoryDir(owner, repository)), "master", ".")
- .filter(_.name.endsWith(".md"))
- .map(_.name.replaceFirst("\\.md$", ""))
- .sortBy(x => x)
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
+ JGitUtil.getFileList(git, "master", ".")
+ .filter(_.name.endsWith(".md"))
+ .map(_.name.replaceFirst("\\.md$", ""))
+ .sortBy(x => x)
+ }
}
-
+
+ /**
+ * Reverts specified changes.
+ */
+ def revertWikiPage(owner: String, repository: String, from: String, to: String,
+ committer: model.Account, pageName: Option[String]): Boolean = {
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
+ defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
+ // clone working copy
+ cloneOrPullWorkingCopy(workDir, owner, repository)
+
+ using(Git.open(workDir)){ git =>
+ val reader = git.getRepository.newObjectReader
+ val oldTreeIter = new CanonicalTreeParser
+ oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
+
+ val newTreeIter = new CanonicalTreeParser
+ newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
+
+ import scala.collection.JavaConverters._
+ val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff =>
+ pageName match {
+ case Some(x) => diff.getNewPath == x + ".md"
+ case None => true
+ }
+ }
+
+ val patch = using(new java.io.ByteArrayOutputStream()){ out =>
+ val formatter = new DiffFormatter(out)
+ formatter.setRepository(git.getRepository)
+ formatter.format(diffs.asJava)
+ new String(out.toByteArray, "UTF-8")
+ }
+
+ try {
+ git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call
+ git.add.addFilepattern(".").call
+ git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match {
+ case Some(x) => s"Revert ${from} ... ${to} on ${x}"
+ case None => s"Revert ${from} ... ${to}"
+ }).call
+ git.push.call
+ true
+ } catch {
+ case ex: PatchApplyException => false
+ }
+ }
+ }
+ }
+ }
+
+
/**
* Save the wiki page.
*/
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
- content: String, committer: model.Account, message: String): Unit = {
+ content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
- lock(owner, repository){
- // clone working copy
- val workDir = Directory.getWikiWorkDir(owner, repository)
- cloneOrPullWorkingCopy(workDir, owner, repository)
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
+ defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
+ // clone working copy
+ cloneOrPullWorkingCopy(workDir, owner, repository)
- // write as file
- JGitUtil.withGit(workDir){ git =>
- val file = new File(workDir, newPageName + ".md")
- val added = if(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
- FileUtils.writeStringToFile(file, content, "UTF-8")
- git.add.addFilepattern(file.getName).call
- true
- } else {
- false
- }
+ // write as file
+ using(Git.open(workDir)){ git =>
+ defining(new File(workDir, newPageName + ".md")){ file =>
+ // new page
+ val created = !file.exists
- // delete file
- val deleted = if(currentPageName != "" && currentPageName != newPageName){
- git.rm.addFilepattern(currentPageName + ".md").call
- true
- } else {
- false
- }
+ // created or updated
+ val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){
+ FileUtils.writeStringToFile(file, content, "UTF-8")
+ git.add.addFilepattern(file.getName).call
+ }
- // commit and push
- if(added || deleted){
- git.commit.setCommitter(committer.userName, committer.mailAddress).setMessage(message).call
- git.push.call
+ // delete file
+ val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){
+ git.rm.addFilepattern(currentPageName + ".md").call
+ }
+
+ // commit and push
+ optionIf(added || deleted){
+ defining(git.commit.setCommitter(committer.fullName, committer.mailAddress)
+ .setMessage(if(message.trim.length == 0){
+ if(deleted){
+ s"Rename ${currentPageName} to ${newPageName}"
+ } else if(created){
+ s"Created ${newPageName}"
+ } else {
+ s"Updated ${newPageName}"
+ }
+ } else {
+ message
+ }).call){ commit =>
+ git.push.call
+ Some(commit.getName)
+ }
+ }
+ }
}
}
}
@@ -167,56 +200,38 @@
/**
* Delete the wiki page.
*/
- def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = {
- lock(owner, repository){
- // clone working copy
- val workDir = Directory.getWikiWorkDir(owner, repository)
- cloneOrPullWorkingCopy(workDir, owner, repository)
+ def deleteWikiPage(owner: String, repository: String, pageName: String,
+ committer: String, mailAddress: String, message: String): Unit = {
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
+ defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
+ // clone working copy
+ cloneOrPullWorkingCopy(workDir, owner, repository)
- // delete file
- new File(workDir, pageName + ".md").delete
-
- JGitUtil.withGit(workDir){ git =>
- git.rm.addFilepattern(pageName + ".md").call
-
- // commit and push
- // TODO committer's mail address
- git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call
- git.push.call
+ // delete file
+ new File(workDir, pageName + ".md").delete
+
+ using(Git.open(workDir)){ git =>
+ git.rm.addFilepattern(pageName + ".md").call
+
+ // commit and push
+ git.commit.setCommitter(committer, mailAddress).setMessage(message).call
+ git.push.call
+ }
}
}
}
- /**
- * Returns differences between specified commits.
- */
- def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = {
- // get diff between specified commit and its previous commit
- val reader = git.getRepository.newObjectReader
-
- val oldTreeIter = new CanonicalTreeParser
- oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}"))
-
- val newTreeIter = new CanonicalTreeParser
- newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}"))
-
- import scala.collection.JavaConverters._
- git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
- DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
- JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")),
- JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8")))
- }.toList
- }
-
private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = {
if(!workDir.exists){
Git.cloneRepository
.setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString)
.setDirectory(workDir)
.call
- } else {
- Git.open(workDir).pull.call
+ .getRepository
+ .close
+ } else using(Git.open(workDir)){ git =>
+ git.pull.call
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index 4614230..1f78018 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -1,12 +1,14 @@
package servlet
import java.io.File
-import java.sql.Connection
+import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils
-import javax.servlet.ServletContextEvent
+import javax.servlet.{ServletContext, ServletContextListener, ServletContextEvent}
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
-import util.Directory
+import util.Directory._
+import util.ControlUtil._
+import org.eclipse.jgit.api.Git
object AutoUpdate {
@@ -26,15 +28,14 @@
*/
def update(conn: Connection): Unit = {
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
- val in = Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)
- if(in != null){
- val sql = IOUtils.toString(in, "UTF-8")
- val stmt = conn.createStatement()
- try {
- logger.debug(sqlPath + "=" + sql)
- stmt.executeUpdate(sql)
- } finally {
- stmt.close()
+
+ using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
+ if(in != null){
+ val sql = IOUtils.toString(in, "UTF-8")
+ using(conn.createStatement()){ stmt =>
+ logger.debug(sqlPath + "=" + sql)
+ stmt.executeUpdate(sql)
+ }
}
}
}
@@ -49,26 +50,32 @@
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
+ Version(1, 7),
+ Version(1, 6),
+ Version(1, 5),
+ Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
// Fix wiki repository configuration
- val rs = conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")
- while(rs.next){
- val wikidir = Directory.getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))
- val repository = org.eclipse.jgit.api.Git.open(wikidir).getRepository
- val config = repository.getConfig
- if(!config.getBoolean("http", "receivepack", false)){
- config.setBoolean("http", null, "receivepack", true)
- config.save
+ using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
+ while(rs.next){
+ using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
+ defining(git.getRepository.getConfig){ config =>
+ if(!config.getBoolean("http", "receivepack", false)){
+ config.setBoolean("http", null, "receivepack", true)
+ config.save
+ }
+ }
+ }
}
- repository.close
}
}
},
Version(1, 2),
Version(1, 1),
- Version(1, 0)
+ Version(1, 0),
+ Version(0, 0)
)
/**
@@ -79,7 +86,7 @@
/**
* The version file (GITBUCKET_HOME/version).
*/
- val versionFile = new File(Directory.GitBucketHome, "version")
+ val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
@@ -103,35 +110,50 @@
}
/**
- * Start H2 database and update schema automatically.
+ * Update database schema automatically in the context initializing.
*/
-class AutoUpdateListener extends org.h2.server.web.DbStarter {
+class AutoUpdateListener extends ServletContextListener {
import AutoUpdate._
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
override def contextInitialized(event: ServletContextEvent): Unit = {
- super.contextInitialized(event)
- logger.debug("H2 started")
-
+ org.h2.Driver.load()
+ event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}")
+
logger.debug("Start schema update")
- val conn = getConnection()
- try {
- val currentVersion = getCurrentVersion()
- if(currentVersion == headVersion){
- logger.debug("No update")
- } else {
- versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
- FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
- conn.commit()
- logger.debug("Updated from " + currentVersion.versionString + " to " + headVersion.versionString)
- }
- } catch {
- case ex: Throwable => {
- logger.error("Failed to schema update", ex)
- conn.rollback()
+ defining(getConnection(event.getServletContext)){ conn =>
+ try {
+ defining(getCurrentVersion()){ currentVersion =>
+ if(currentVersion == headVersion){
+ logger.debug("No update")
+ } else if(!versions.contains(currentVersion)){
+ logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
+ } else {
+ versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
+ FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
+ conn.commit()
+ logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
+ }
+ }
+ } catch {
+ case ex: Throwable => {
+ logger.error("Failed to schema update", ex)
+ ex.printStackTrace()
+ conn.rollback()
+ }
}
}
logger.debug("End schema update")
}
-
-}
\ No newline at end of file
+
+ def contextDestroyed(sce: ServletContextEvent): Unit = {
+ // Nothing to do.
+ }
+
+ private def getConnection(servletContext: ServletContext): Connection =
+ DriverManager.getConnection(
+ servletContext.getInitParameter("db.url"),
+ servletContext.getInitParameter("db.user"),
+ servletContext.getInitParameter("db.password"))
+
+}
diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala
index a69c237..16c516c 100644
--- a/src/main/scala/servlet/BasicAuthenticationFilter.scala
+++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala
@@ -2,14 +2,16 @@
import javax.servlet._
import javax.servlet.http._
-import util.StringUtil._
-import service.{AccountService, RepositoryService}
+import service.{SystemSettingsService, AccountService, RepositoryService}
import org.slf4j.LoggerFactory
+import util.Implicits._
+import util.ControlUtil._
+import util.Keys
/**
* Provides BASIC Authentication for [[servlet.GitRepositoryServlet]].
*/
-class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService {
+class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter])
@@ -26,28 +28,30 @@
}
try {
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- val repositoryOwner = paths(2)
- val repositoryName = paths(3).replaceFirst("\\.git$", "")
-
- getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki", ""), "") match {
- case Some(repository) => {
- if(!request.getRequestURI.endsWith("/git-receive-pack") && !repository.repository.isPrivate){
- chain.doFilter(req, wrappedResponse)
- } else {
- request.getHeader("Authorization") match {
- case null => requireAuth(response)
- case auth => decodeAuthHeader(auth).split(":") match {
- case Array(username, password) if(isWritableUser(username, password, repository)) => {
- request.setAttribute("USER_NAME", username)
- chain.doFilter(req, wrappedResponse)
+ defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
+ getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
+ case Some(repository) => {
+ if(!request.getRequestURI.endsWith("/git-receive-pack") &&
+ !"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
+ chain.doFilter(req, wrappedResponse)
+ } else {
+ request.getHeader("Authorization") match {
+ case null => requireAuth(response)
+ case auth => decodeAuthHeader(auth).split(":") match {
+ case Array(username, password) if(isWritableUser(username, password, repository)) => {
+ request.setAttribute(Keys.Request.UserName, username)
+ chain.doFilter(req, wrappedResponse)
+ }
+ case _ => requireAuth(response)
}
- case _ => requireAuth(response)
}
}
}
+ case None => {
+ logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.")
+ response.sendError(HttpServletResponse.SC_NOT_FOUND)
+ }
}
- case None => response.sendError(HttpServletResponse.SC_NOT_FOUND)
}
} catch {
case ex: Exception => {
@@ -57,12 +61,12 @@
}
}
- private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = {
- getAccountByUserName(username).map { account =>
- account.password == sha1(password) && hasWritePermission(repository.owner, repository.name, Some(account))
- } getOrElse false
- }
-
+ private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean =
+ authenticate(loadSystemSettings(), username, password) match {
+ case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account))
+ case None => false
+ }
+
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala
index 94c432b..e16f48c 100644
--- a/src/main/scala/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/servlet/GitRepositoryServlet.scala
@@ -9,8 +9,13 @@
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
-import util.{JGitUtil, Directory}
+import util.{Keys, JGitUtil, Directory}
+import util.ControlUtil._
+import util.Implicits._
import service._
+import WebHookService._
+import org.eclipse.jgit.api.Git
+import util.JGitUtil.CommitInfo
/**
* Provides Git repository via HTTP.
@@ -24,7 +29,7 @@
override def init(config: ServletConfig): Unit = {
setReceivePackFactory(new GitBucketReceivePackFactory())
-
+
// TODO are there any other ways...?
super.init(new ServletConfig(){
def getInitParameter(name: String): String = name match {
@@ -33,12 +38,14 @@
case name => config.getInitParameter(name)
}
def getInitParameterNames(): java.util.Enumeration[String] = {
- config.getInitParameterNames
+ config.getInitParameterNames
}
-
+
def getServletContext(): ServletContext = config.getServletContext
def getServletName(): String = config.getServletName
});
+
+ super.init(config)
}
}
@@ -49,68 +56,102 @@
override def create(request: HttpServletRequest, db: Repository): ReceivePack = {
val receivePack = new ReceivePack(db)
- val userName = request.getAttribute("USER_NAME").asInstanceOf[String]
+ val userName = request.getAttribute(Keys.Request.UserName).asInstanceOf[String]
logger.debug("requestURI: " + request.getRequestURI)
logger.debug("userName:" + userName)
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- val owner = paths(2)
- val repository = paths(3).replaceFirst("\\.git$", "")
-
- logger.debug("repository:" + owner + "/" + repository)
+ defining(request.paths){ paths =>
+ val owner = paths(1)
+ val repository = paths(2).replaceFirst("\\.git$", "")
+ val baseURL = request.getRequestURL.toString.replaceFirst("/git/.*", "")
- receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName))
- receivePack
+ logger.debug("repository:" + owner + "/" + repository)
+ logger.debug("baseURL:" + baseURL)
+
+ receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, userName, baseURL))
+ receivePack
+ }
}
}
import scala.collection.JavaConverters._
-class CommitLogHook(owner: String, repository: String, userName: String) extends PostReceiveHook
- with RepositoryService with AccountService with IssuesService with ActivityService {
+class CommitLogHook(owner: String, repository: String, userName: String, baseURL: String) extends PostReceiveHook
+ with RepositoryService with AccountService with IssuesService with ActivityService with WebHookService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
- JGitUtil.withGit(Directory.getRepositoryDir(owner, repository)) { git =>
+ using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
commands.asScala.foreach { command =>
val commits = JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name)
val refName = command.getRefName.split("/")
-
- // apply issue comment
- val newCommits = commits.flatMap { commit =>
- if(!existsCommitId(owner, repository, commit.id)){
- insertCommitId(owner, repository, commit.id)
- "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
- val issueId = matchData.group(2)
- if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
- createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
- }
+ val branchName = refName.drop(2).mkString("/")
+
+ // Extract new commit and apply issue comment
+ val newCommits = if(commits.size > 1000){
+ val existIds = getAllCommitIds(owner, repository)
+ commits.flatMap { commit =>
+ optionIf(!existIds.contains(commit.id)){
+ createIssueComment(commit)
+ Some(commit)
}
- Some(commit)
- } else None
- }.toList
-
+ }
+ } else {
+ commits.flatMap { commit =>
+ optionIf(!existsCommitId(owner, repository, commit.id)){
+ createIssueComment(commit)
+ Some(commit)
+ }
+ }
+ }
+
+ // batch insert all new commit id
+ insertAllCommitIds(owner, repository, newCommits.map(_.id))
+
// record activity
if(refName(1) == "heads"){
command.getType match {
case ReceiveCommand.Type.CREATE => {
- recordCreateBranchActivity(owner, repository, userName, refName(2))
- recordPushActivity(owner, repository, userName, refName(2), newCommits)
+ recordCreateBranchActivity(owner, repository, userName, branchName)
+ recordPushActivity(owner, repository, userName, branchName, newCommits)
}
- case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, refName(2), newCommits)
+ case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, userName, branchName, newCommits)
case _ =>
}
} else if(refName(1) == "tags"){
command.getType match {
- case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, refName(2), newCommits)
+ case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, userName, branchName, newCommits)
case _ =>
}
}
+
+ // call web hook
+ val webHookURLs = getWebHookURLs(owner, repository)
+ if(webHookURLs.nonEmpty){
+ val payload = WebHookPayload(
+ git,
+ command.getRefName,
+ getRepository(owner, repository, baseURL).get,
+ newCommits,
+ getAccountByUserName(owner).get)
+
+ callWebHook(owner, repository, webHookURLs, payload)
+ }
}
}
// update repository last modified time.
updateLastActivityDate(owner, repository)
}
+
+ private def createIssueComment(commit: CommitInfo) = {
+ "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(commit.fullMessage).matchData.foreach { matchData =>
+ val issueId = matchData.group(2)
+ if(getAccountByUserName(commit.committer).isDefined && getIssue(owner, repository, issueId).isDefined){
+ createComment(owner, repository, commit.committer, issueId.toInt, commit.fullMessage, "commit")
+ }
+ }
+ }
+
}
diff --git a/src/main/scala/servlet/SessionCleanupListener.scala b/src/main/scala/servlet/SessionCleanupListener.scala
index 86c8bcc..87ce1d1 100644
--- a/src/main/scala/servlet/SessionCleanupListener.scala
+++ b/src/main/scala/servlet/SessionCleanupListener.scala
@@ -1,17 +1,15 @@
package servlet
-import util.FileUploadUtil
import javax.servlet.http.{HttpSessionEvent, HttpSessionListener}
+import app.FileUploadControllerBase
/**
* Removes session associated temporary files when session is destroyed.
*/
-class SessionCleanupListener extends HttpSessionListener {
+class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase {
def sessionCreated(se: HttpSessionEvent): Unit = {}
- def sessionDestroyed(se: HttpSessionEvent): Unit = {
- FileUploadUtil.removeTemporaryFiles()(se.getSession)
- }
+ def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession)
}
diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala
index 5a26734..12363ad 100644
--- a/src/main/scala/servlet/TransactionFilter.scala
+++ b/src/main/scala/servlet/TransactionFilter.scala
@@ -3,7 +3,6 @@
import javax.servlet._
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
-import scala.slick.session.Database
/**
* Controls the transaction with the open session in view pattern.
@@ -21,15 +20,19 @@
// assets don't need transaction
chain.doFilter(req, res)
} else {
- val context = req.getServletContext
- Database.forURL(context.getInitParameter("db.url"),
- context.getInitParameter("db.user"),
- context.getInitParameter("db.password")) withTransaction {
- logger.debug("TODO begin transaction")
+ Database(req.getServletContext) withTransaction {
+ logger.debug("begin transaction")
chain.doFilter(req, res)
- logger.debug("TODO end transaction")
+ logger.debug("end transaction")
}
}
}
-
-}
\ No newline at end of file
+
+}
+
+object Database {
+ def apply(context: ServletContext): scala.slick.session.Database =
+ scala.slick.session.Database.forURL(context.getInitParameter("db.url"),
+ context.getInitParameter("db.user"),
+ context.getInitParameter("db.password"))
+}
diff --git a/src/main/scala/util/Authenticator.scala b/src/main/scala/util/Authenticator.scala
index b22a0d7..c524713 100644
--- a/src/main/scala/util/Authenticator.scala
+++ b/src/main/scala/util/Authenticator.scala
@@ -3,6 +3,8 @@
import app.ControllerBase
import service._
import RepositoryService.RepositoryInfo
+import util.Implicits._
+import util.ControlUtil._
/**
* Allows only oneself and administrators.
@@ -13,11 +15,12 @@
private def authenticate(action: => Any) = {
{
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action
- case Some(x) if(paths(1) == x.userName) => action
- case _ => Unauthorized()
+ defining(request.paths){ paths =>
+ context.loginAccount match {
+ case Some(x) if(x.isAdmin) => action
+ case Some(x) if(paths(0) == x.userName) => action
+ case _ => Unauthorized()
+ }
}
}
}
@@ -32,14 +35,15 @@
private def authenticate(action: (RepositoryInfo) => Any) = {
{
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- getRepository(paths(1), paths(2), baseUrl).map { repository =>
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action(repository)
- case Some(x) if(repository.owner == x.userName) => action(repository)
- case _ => Unauthorized()
- }
- } getOrElse NotFound()
+ defining(request.paths){ paths =>
+ getRepository(paths(0), paths(1), baseUrl).map { repository =>
+ context.loginAccount match {
+ case Some(x) if(x.isAdmin) => action(repository)
+ case Some(x) if(repository.owner == x.userName) => action(repository)
+ case _ => Unauthorized()
+ }
+ } getOrElse NotFound()
+ }
}
}
}
@@ -87,15 +91,16 @@
private def authenticate(action: (RepositoryInfo) => Any) = {
{
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- getRepository(paths(1), paths(2), baseUrl).map { repository =>
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action(repository)
- case Some(x) if(paths(1) == x.userName) => action(repository)
- case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
- case _ => Unauthorized()
- }
- } getOrElse NotFound()
+ defining(request.paths){ paths =>
+ getRepository(paths(0), paths(1), baseUrl).map { repository =>
+ context.loginAccount match {
+ case Some(x) if(x.isAdmin) => action(repository)
+ case Some(x) if(paths(0) == x.userName) => action(repository)
+ case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
+ case _ => Unauthorized()
+ }
+ } getOrElse NotFound()
+ }
}
}
}
@@ -109,19 +114,20 @@
private def authenticate(action: (RepositoryInfo) => Any) = {
{
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- getRepository(paths(1), paths(2), baseUrl).map { repository =>
- if(!repository.repository.isPrivate){
- action(repository)
- } else {
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action(repository)
- case Some(x) if(paths(1) == x.userName) => action(repository)
- case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
- case _ => Unauthorized()
+ defining(request.paths){ paths =>
+ getRepository(paths(0), paths(1), baseUrl).map { repository =>
+ if(!repository.repository.isPrivate){
+ action(repository)
+ } else {
+ context.loginAccount match {
+ case Some(x) if(x.isAdmin) => action(repository)
+ case Some(x) if(paths(0) == x.userName) => action(repository)
+ case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
+ case _ => Unauthorized()
+ }
}
- }
- } getOrElse NotFound()
+ } getOrElse NotFound()
+ }
}
}
}
@@ -135,16 +141,17 @@
private def authenticate(action: (RepositoryInfo) => Any) = {
{
- val paths = request.getRequestURI.substring(request.getContextPath.length).split("/")
- getRepository(paths(1), paths(2), baseUrl).map { repository =>
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action(repository)
- case Some(x) if(!repository.repository.isPrivate) => action(repository)
- case Some(x) if(paths(1) == x.userName) => action(repository)
- case Some(x) if(getCollaborators(paths(1), paths(2)).contains(x.userName)) => action(repository)
- case _ => Unauthorized()
- }
- } getOrElse NotFound()
+ defining(request.paths){ paths =>
+ getRepository(paths(0), paths(1), baseUrl).map { repository =>
+ context.loginAccount match {
+ case Some(x) if(x.isAdmin) => action(repository)
+ case Some(x) if(!repository.repository.isPrivate) => action(repository)
+ case Some(x) if(paths(0) == x.userName) => action(repository)
+ case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository)
+ case _ => Unauthorized()
+ }
+ } getOrElse NotFound()
+ }
}
}
}
diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala
new file mode 100644
index 0000000..c7b4310
--- /dev/null
+++ b/src/main/scala/util/ControlUtil.scala
@@ -0,0 +1,48 @@
+package util
+
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.revwalk.RevWalk
+import org.eclipse.jgit.treewalk.TreeWalk
+
+/**
+ * Provides control facilities.
+ */
+object ControlUtil {
+
+ def defining[A, B](value: A)(f: A => B): B = f(value)
+
+ def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B =
+ try f(resource) finally {
+ if(resource != null){
+ try {
+ resource.close()
+ } catch {
+ case e: Throwable => // ignore
+ }
+ }
+ }
+
+ def using[T](git: Git)(f: Git => T): T =
+ try f(git) finally git.getRepository.close
+
+ def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T =
+ try f(git1, git2) finally {
+ git1.getRepository.close
+ git2.getRepository.close
+ }
+
+ def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
+ try f(revWalk) finally revWalk.release()
+
+ def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
+ try f(treeWalk) finally treeWalk.release()
+
+ def executeIf(condition: => Boolean)(action: => Unit): Boolean =
+ if(condition){
+ action
+ true
+ } else false
+
+ def optionIf[T](condition: => Boolean)(action: => Option[T]): Option[T] =
+ if(condition) action else None
+}
diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala
index f2b7f4c..5409962 100644
--- a/src/main/scala/util/Directory.scala
+++ b/src/main/scala/util/Directory.scala
@@ -1,34 +1,38 @@
package util
import java.io.File
-import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.lib.Ref
+import util.ControlUtil._
/**
* Provides directories used by GitBucket.
*/
object Directory {
- val GitBucketHome = new File(System.getProperty("user.home"), "gitbucket").getAbsolutePath
+ val GitBucketHome = (scala.util.Properties.envOrNone("GITBUCKET_HOME") match {
+ case Some(env) => new File(env)
+ case None => new File(System.getProperty("user.home"), "gitbucket")
+ }).getAbsolutePath
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
val RepositoryHome = s"${GitBucketHome}/repositories"
+
+ val DatabaseHome = s"${GitBucketHome}/data"
/**
* Repository names of the specified user.
*/
- def getRepositories(owner: String): List[String] = {
- val dir = new File(s"${RepositoryHome}/${owner}")
- if(dir.exists){
- dir.listFiles.filter { file =>
- file.isDirectory && !file.getName.endsWith(".wiki.git")
- }.map(_.getName.replaceFirst("\\.git$", "")).toList
- } else {
- Nil
+ def getRepositories(owner: String): List[String] =
+ defining(new File(s"${RepositoryHome}/${owner}")){ dir =>
+ if(dir.exists){
+ dir.listFiles.filter { file =>
+ file.isDirectory && !file.getName.endsWith(".wiki.git")
+ }.map(_.getName.replaceFirst("\\.git$", "")).toList
+ } else {
+ Nil
+ }
}
- }
-
+
/**
* Substance directory of the repository.
*/
diff --git a/src/main/scala/util/FileUploadUtil.scala b/src/main/scala/util/FileUploadUtil.scala
deleted file mode 100644
index 6efacc1..0000000
--- a/src/main/scala/util/FileUploadUtil.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-package util
-
-import java.text.SimpleDateFormat
-import javax.servlet.http.HttpSession
-import util.Directory._
-import org.apache.commons.io.FileUtils
-
-object FileUploadUtil {
-
- def generateFileId: String =
- new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis))
-
- def TemporaryDir(implicit session: HttpSession): java.io.File =
- new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}")
-
- def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File =
- new java.io.File(TemporaryDir, fileId)
-
-// def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit =
-// getTemporaryFile(fileId).delete()
-
- def removeTemporaryFiles()(implicit session: HttpSession): Unit =
- FileUtils.deleteDirectory(TemporaryDir)
-
- def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = {
- val filename = Option(session.getAttribute("upload_" + fileId).asInstanceOf[String])
- if(filename.isDefined){
- session.removeAttribute("upload_" + fileId)
- }
- filename
- }
-
-}
diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala
index 01f8874..1386a05 100644
--- a/src/main/scala/util/FileUtil.scala
+++ b/src/main/scala/util/FileUtil.scala
@@ -1,22 +1,31 @@
package util
-import org.apache.commons.io.{IOUtils, FileUtils, FilenameUtils}
+import org.apache.commons.io.{IOUtils, FileUtils}
import java.net.URLConnection
import java.io.File
import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream}
+import util.ControlUtil._
object FileUtil {
- def getMimeType(name: String): String = {
- val fileNameMap = URLConnection.getFileNameMap()
- val mimeType = fileNameMap.getContentTypeFor(name)
- if(mimeType == null){
- "application/octeat-stream"
- } else {
- mimeType
+ def getMimeType(name: String): String =
+ defining(URLConnection.getFileNameMap()){ fileNameMap =>
+ fileNameMap.getContentTypeFor(name) match {
+ case null => "application/octet-stream"
+ case mimeType => mimeType
+ }
+ }
+
+ def getContentType(name: String, bytes: Array[Byte]): String = {
+ defining(getMimeType(name)){ mimeType =>
+ if(mimeType == "application/octet-stream" && isText(bytes)){
+ "text/plain"
+ } else {
+ mimeType
+ }
}
}
-
+
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
@@ -36,21 +45,29 @@
}
}
- val out = new ZipArchiveOutputStream(dest)
- try {
+ using(new ZipArchiveOutputStream(dest)){ out =>
addDirectoryToZip(out, dir, dir.getName)
- } finally {
- IOUtils.closeQuietly(out)
}
}
- def getExtension(name: String): String = {
- val index = name.lastIndexOf('.')
- if(index >= 0){
- name.substring(index + 1)
- } else {
- ""
- }
+ def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i =>
+ if(i >= 0) path.substring(i + 1) else path
}
-}
\ No newline at end of file
+ def getExtension(name: String): String =
+ name.lastIndexOf('.') match {
+ case i if(i >= 0) => name.substring(i + 1)
+ case _ => ""
+ }
+
+ def withTmpDir[A](dir: File)(action: File => A): A = {
+ if(dir.exists()){
+ FileUtils.deleteDirectory(dir)
+ }
+ try{
+ action(dir)
+ }finally{
+ FileUtils.deleteDirectory(dir)
+ }
+ }
+}
diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala
index c475136..a9e648c 100644
--- a/src/main/scala/util/Implicits.scala
+++ b/src/main/scala/util/Implicits.scala
@@ -1,7 +1,7 @@
package util
-import scala.slick.driver.H2Driver.simple._
import scala.util.matching.Regex
+import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
* Provides some usable implicit conversions.
@@ -13,7 +13,7 @@
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@scala.annotation.tailrec
- private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = {
+ private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] =
list match {
case x :: xs => {
xs.span(condition(x, _)) match {
@@ -22,12 +22,6 @@
}
case Nil => result
}
- }
- }
-
- // TODO Should this implicit conversion move to model.Functions?
- implicit class RichColumn(c1: Column[Boolean]){
- def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1
}
implicit class RichString(value: String){
@@ -47,6 +41,38 @@
}
sb.toString
}
+
+ def toIntOpt: Option[Int] = try {
+ Option(Integer.parseInt(value))
+ } catch {
+ case e: NumberFormatException => None
+ }
}
-}
\ No newline at end of file
+ implicit class RichRequest(request: HttpServletRequest){
+
+ def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
+
+ def hasQueryString: Boolean = request.getQueryString != null
+
+ def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
+
+ }
+
+ implicit class RichSession(session: HttpSession){
+
+ def putAndGet[T](key: String, value: T): T = {
+ session.setAttribute(key, value)
+ value
+ }
+
+ def getAndRemove[T](key: String): Option[T] = {
+ val value = session.getAttribute(key).asInstanceOf[T]
+ if(value == null){
+ session.removeAttribute(key)
+ }
+ Option(value)
+ }
+ }
+
+}
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index 4f9091c..c282e9a 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -2,19 +2,19 @@
import org.eclipse.jgit.api.Git
import util.Directory._
+import util.StringUtil._
+import util.ControlUtil._
import scala.collection.JavaConverters._
-import javax.servlet.ServletContext
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
-import org.eclipse.jgit.diff._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
-import org.eclipse.jgit.util.io.DisabledOutputStream
import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
+import service.RepositoryService
/**
* Provides complex JGit operations.
@@ -71,29 +71,17 @@
rev.getFullMessage,
rev.getParents().map(_.name).toList)
- val summary = {
- val i = fullMessage.trim.indexOf("\n")
- val firstLine = if(i >= 0){
- fullMessage.trim.substring(0, i).trim
- } else {
- fullMessage
- }
- if(firstLine.length > shortMessage.length){
- shortMessage
- } else {
- firstLine
+ val summary = defining(fullMessage.trim.indexOf("\n")){ i =>
+ defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
+ if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
}
- val description = {
- val i = fullMessage.trim.indexOf("\n")
- if(i >= 0){
+ val description = defining(fullMessage.trim.indexOf("\n")){ i =>
+ optionIf(i >= 0){
Some(fullMessage.trim.substring(i).trim)
- } else {
- None
}
}
-
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
@@ -116,33 +104,18 @@
case class TagInfo(name: String, time: Date, id: String)
/**
- * Use this method to use the Git object.
- * Repository resources are released certainly after processing.
- */
- def withGit[T](dir: java.io.File)(f: Git => T): T = withGit(Git.open(dir))(f)
-
- /**
- * Use this method to use the Git object.
- * Repository resources are released certainly after processing.
- */
- def withGit[T](git: Git)(f: Git => T): T = {
- try {
- f(git)
- } finally {
- git.getRepository.close
- }
- }
-
- /**
- * Returns RevCommit from the commit id.
+ * Returns RevCommit from the commit or tag id.
*
* @param git the Git object
- * @param commitId the ObjectId of the commit
- * @return the RevCommit for the specified commit
+ * @param objectId the ObjectId of the commit or tag
+ * @return the RevCommit for the specified commit or tag
*/
- def getRevCommitFromId(git: Git, commitId: ObjectId): RevCommit = {
+ def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = {
val revWalk = new RevWalk(git.getRepository)
- val revCommit = revWalk.parseCommit(commitId)
+ val revCommit = revWalk.parseAny(objectId) match {
+ case r: RevTag => revWalk.parseCommit(r.getObject)
+ case _ => revWalk.parseCommit(objectId)
+ }
revWalk.dispose
revCommit
}
@@ -151,15 +124,10 @@
* Returns the repository information. It contains branch names and tag names.
*/
def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = {
- withGit(getRepositoryDir(owner, repository)){ git =>
+ using(Git.open(getRepositoryDir(owner, repository))){ git =>
try {
// get commit count
- val i = git.log.all.call.iterator
- var commitCount = 0
- while(i.hasNext && commitCount <= 1000){
- i.next
- commitCount = commitCount + 1
- }
+ val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum
RepositoryInfo(
owner, repository, s"${baseUrl}/git/${owner}/${repository}.git",
@@ -193,45 +161,43 @@
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
- val revWalk = new RevWalk(git.getRepository)
- val objectId = git.getRepository.resolve(revision)
- val revCommit = revWalk.parseCommit(objectId)
-
- val treeWalk = new TreeWalk(git.getRepository)
- treeWalk.addTree(revCommit.getTree)
- if(path != "."){
- treeWalk.setRecursive(true)
- treeWalk.setFilter(new TreeFilter(){
-
- var stopRecursive = false
-
- def include(walker: TreeWalk): Boolean = {
- val targetPath = walker.getPathString
- if((path + "/").startsWith(targetPath)){
- true
- } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
- stopRecursive = true
- treeWalk.setRecursive(false)
- true
- } else {
- false
- }
- }
-
- def shouldBeRecursive(): Boolean = !stopRecursive
-
- override def clone: TreeFilter = return this
- })
- }
-
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)]
-
- while (treeWalk.next()) {
- list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
+
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ val objectId = git.getRepository.resolve(revision)
+ val revCommit = revWalk.parseCommit(objectId)
+
+ using(new TreeWalk(git.getRepository)){ treeWalk =>
+ treeWalk.addTree(revCommit.getTree)
+ if(path != "."){
+ treeWalk.setRecursive(true)
+ treeWalk.setFilter(new TreeFilter(){
+
+ var stopRecursive = false
+
+ def include(walker: TreeWalk): Boolean = {
+ val targetPath = walker.getPathString
+ if((path + "/").startsWith(targetPath)){
+ true
+ } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf("/") < 0){
+ stopRecursive = true
+ treeWalk.setRecursive(false)
+ true
+ } else {
+ false
+ }
+ }
+
+ def shouldBeRecursive(): Boolean = !stopRecursive
+
+ override def clone: TreeFilter = return this
+ })
+ }
+ while (treeWalk.next()) {
+ list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString))
+ }
+ }
}
-
- treeWalk.release
- revWalk.dispose
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name) =>
@@ -261,7 +227,7 @@
* @param page the page number (1-)
* @param limit the number of commit info per page. 0 (default) means unlimited.
* @param path filters by this path. default is no filter.
- * @return a tuple of the commit list and whether has next
+ * @return a tuple of the commit list and whether has next, or the error message
*/
def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = {
val fixedPage = if(page <= 0) 1 else page
@@ -276,27 +242,48 @@
case _ => (logs, i.hasNext)
}
- val revWalk = new RevWalk(git.getRepository)
- val objectId = git.getRepository.resolve(revision)
- if(objectId == null){
- Left(s"${revision} can't be resolved.")
- } else {
- revWalk.markStart(revWalk.parseCommit(objectId))
- if(path.nonEmpty){
- revWalk.setRevFilter(new RevFilter(){
- def include(walk: RevWalk, commit: RevCommit): Boolean = {
- getDiffs(git, commit.getName, false).find(_.newPath == path).nonEmpty
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ defining(git.getRepository.resolve(revision)){ objectId =>
+ if(objectId == null){
+ Left(s"${revision} can't be resolved.")
+ } else {
+ revWalk.markStart(revWalk.parseCommit(objectId))
+ if(path.nonEmpty){
+ revWalk.setRevFilter(new RevFilter(){
+ def include(walk: RevWalk, commit: RevCommit): Boolean = {
+ getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
+ }
+ override def clone(): RevFilter = this
+ })
}
- override def clone(): RevFilter = this
- })
+ Right(getCommitLog(revWalk.iterator, 0, Nil))
+ }
}
-
- val commits = getCommitLog(revWalk.iterator, 0, Nil)
- revWalk.release
-
- Right(commits)
}
}
+
+ def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false)
+ (endCondition: RevCommit => Boolean): List[CommitInfo] = {
+ @scala.annotation.tailrec
+ def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
+ i.hasNext match {
+ case true => {
+ val revCommit = i.next
+ if(endCondition(revCommit)){
+ if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs
+ } else {
+ getCommitLog(i, logs :+ new CommitInfo(revCommit))
+ }
+ }
+ case false => logs
+ }
+
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
+ getCommitLog(revWalk.iterator, Nil).reverse
+ }
+ }
+
/**
* Returns the commit list between two revisions.
@@ -306,30 +293,9 @@
* @param to the to revision
* @return the commit list
*/
- def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = {
- @scala.annotation.tailrec
- def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] =
- i.hasNext match {
- case true => {
- val revCommit = i.next
- if(revCommit.name == from){
- logs
- } else {
- getCommitLog(i, logs :+ new CommitInfo(revCommit))
- }
- }
- case false => logs
- }
-
- val revWalk = new RevWalk(git.getRepository)
- revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(to)))
-
- val commits = getCommitLog(revWalk.iterator, Nil)
- revWalk.release
-
- commits.reverse
- }
-
+ // TODO swap parameters 'from' and 'to'!?
+ def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] =
+ getCommitLogs(git, to)(_.getName == from)
/**
* Returns the latest RevCommit of the specified path.
@@ -351,51 +317,11 @@
* @return the list of latest commit
*/
def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = {
-
- val map = new scala.collection.mutable.HashMap[String, RevCommit]
-
- val revWalk = new RevWalk(git.getRepository)
- revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(revision)))
- //revWalk.sort(RevSort.REVERSE);
- val i = revWalk.iterator
-
- while(i.hasNext && map.size != paths.length){
- val commit = i.next
- if(commit.getParentCount == 0){
- // Initial commit
- val treeWalk = new TreeWalk(git.getRepository)
- treeWalk.reset()
- treeWalk.setRecursive(true)
- treeWalk.addTree(commit.getTree)
- while (treeWalk.next) {
- paths.foreach { path =>
- if(treeWalk.getPathString.startsWith(path) && !map.contains(path)){
- map.put(path, commit)
- }
- }
- }
- treeWalk.release
- } else {
- (0 to commit.getParentCount - 1).foreach { i =>
- val parent = revWalk.parseCommit(commit.getParent(i).getId())
- val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
- df.setRepository(git.getRepository)
- df.setDiffComparator(RawTextComparator.DEFAULT)
- df.setDetectRenames(true)
- val diffs = df.scan(parent.getTree(), commit.getTree)
- diffs.asScala.foreach { diff =>
- paths.foreach { path =>
- if(diff.getChangeType != ChangeType.DELETE && diff.getNewPath.startsWith(path) && !map.contains(path)){
- map.put(path, commit)
- }
- }
- }
- }
- }
-
- revWalk.release
- }
- map.toMap
+ val start = getRevCommitFromId(git, git.getRepository.resolve(revision))
+ paths.map { path =>
+ val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next
+ (path, commit)
+ }.toMap
}
/**
@@ -411,126 +337,131 @@
if(large == false && FileUtil.isLarge(loader.getSize)){
None
} else {
- val db = git.getRepository.getObjectDatabase
- try {
+ using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
- } finally {
- db.close
}
}
} catch {
case e: MissingObjectException => None
}
-
- def getDiffs(git: Git, id: String, fetchContent: Boolean = true): List[DiffInfo] = {
+
+ /**
+ * Returns the tuple of diff of the given commit and the previous commit id.
+ */
+ def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match {
case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
case _ => logs
}
-
- val revWalk = new RevWalk(git.getRepository)
- revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
-
- val commits = getCommitLog(revWalk.iterator, Nil)
- revWalk.release
-
- val revCommit = commits(0)
-
- if(commits.length >= 2){
- // not initial commit
- val oldCommit = commits(1)
-
- // get diff between specified commit and its previous commit
- val reader = git.getRepository.newObjectReader
-
- val oldTreeIter = new CanonicalTreeParser
- oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}"))
-
- val newTreeIter = new CanonicalTreeParser
- newTreeIter.reset(reader, git.getRepository.resolve(id + "^{tree}"))
-
- import scala.collection.JavaConverters._
- git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
- if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
- DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
- } else {
- DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
- JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")),
- JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
+
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
+ val commits = getCommitLog(revWalk.iterator, Nil)
+ val revCommit = commits(0)
+
+ if(commits.length >= 2){
+ // not initial commit
+ val oldCommit = commits(1)
+ (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
+
+ } else {
+ // initial commit
+ using(new TreeWalk(git.getRepository)){ treeWalk =>
+ treeWalk.addTree(revCommit.getTree)
+ val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
+ while(treeWalk.next){
+ buffer.append((if(!fetchContent){
+ DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
+ } else {
+ DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
+ JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
+ }))
+ }
+ (buffer.toList, None)
}
- }.toList
- } else {
- // initial commit
- val walk = new TreeWalk(git.getRepository)
- walk.addTree(revCommit.getTree)
- val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
- while(walk.next){
- buffer.append((if(!fetchContent){
- DiffInfo(ChangeType.ADD, null, walk.getPathString, None, None)
- } else {
- DiffInfo(ChangeType.ADD, null, walk.getPathString, None,
- JGitUtil.getContent(git, walk.getObjectId(0), false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
- }))
}
- walk.release
- buffer.toList
}
}
+ def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
+ val reader = git.getRepository.newObjectReader
+ val oldTreeIter = new CanonicalTreeParser
+ oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
+
+ val newTreeIter = new CanonicalTreeParser
+ newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
+
+ import scala.collection.JavaConverters._
+ git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
+ if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
+ DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
+ } else {
+ DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
+ JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
+ JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
+ }
+ }.toList
+ }
+
+
/**
* Returns the list of branch names of the specified commit.
*/
- def getBranchesOfCommit(git: Git, commitId: String): List[String] = {
- val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
- try {
- val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
-
- git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
- (e.getKey.startsWith(Constants.R_HEADS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
- }.map { e =>
- e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
- }.toList.sorted
-
- } finally {
- walk.release
+ def getBranchesOfCommit(git: Git, commitId: String): List[String] =
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
+ git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
+ (e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
+ }.map { e =>
+ e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length)
+ }.toList.sorted
+ }
}
- }
/**
* Returns the list of tags of the specified commit.
*/
- def getTagsOfCommit(git: Git, commitId: String): List[String] = {
- val walk = new org.eclipse.jgit.revwalk.RevWalk(git.getRepository)
- try {
- val commit = walk.parseCommit(git.getRepository.resolve(commitId + "^0"))
-
- git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
- (e.getKey.startsWith(Constants.R_TAGS) && walk.isMergedInto(commit, walk.parseCommit(e.getValue.getObjectId)))
- }.map { e =>
- e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
- }.toList.sorted.reverse
-
- } finally {
- walk.release
+ def getTagsOfCommit(git: Git, commitId: String): List[String] =
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit =>
+ git.getRepository.getAllRefs.entrySet.asScala.filter { e =>
+ (e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId)))
+ }.map { e =>
+ e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length)
+ }.toList.sorted.reverse
+ }
}
- }
- def initRepository(dir: java.io.File): Unit = {
- val repository = new RepositoryBuilder().setGitDir(dir).setBare.build
- try {
+ def initRepository(dir: java.io.File): Unit =
+ using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
repository.create
setReceivePack(repository)
- } finally {
- repository.close
}
+
+ def cloneRepository(from: java.io.File, to: java.io.File): Unit =
+ using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git =>
+ setReceivePack(git.getRepository)
+ }
+
+ def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
+
+ private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit =
+ defining(repository.getConfig){ config =>
+ config.setBoolean("http", null, "receivepack", true)
+ config.save
+ }
+
+ def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
+ revstr: String = ""): Option[(ObjectId, String)] = {
+ Seq(
+ Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr),
+ repository.branchList.headOption
+ ).flatMap {
+ case Some(rev) => Some((git.getRepository.resolve(rev), rev))
+ case None => None
+ }.find(_._1 != null)
}
- private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
- val config = repository.getConfig
- config.setBoolean("http", null, "receivepack", true)
- config.save
- }
-
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/util/Keys.scala b/src/main/scala/util/Keys.scala
new file mode 100644
index 0000000..4aabe35
--- /dev/null
+++ b/src/main/scala/util/Keys.scala
@@ -0,0 +1,72 @@
+package util
+
+/**
+ * Define key strings for request attributes, session attributes or flash attributes.
+ */
+object Keys {
+
+ /**
+ * Define session keys.
+ */
+ object Session {
+
+ /**
+ * Session key for the logged in account information.
+ */
+ val LoginAccount = "LOGIN_ACCOUNT"
+
+ /**
+ * Session key for the redirect URL.
+ */
+ val Redirect = "REDIRECT"
+
+ /**
+ * Session key for the issue search condition in dashboard.
+ */
+ val DashboardIssues = "dashboard/issues"
+
+ /**
+ * Session key for the pull request search condition in dashboard.
+ */
+ val DashboardPulls = "dashboard/pulls"
+
+ /**
+ * Generate session key for the issue search condition.
+ */
+ def Issues(owner: String, name: String) = s"${owner}/${name}/issues"
+
+ /**
+ * Generate session key for the pull request search condition.
+ */
+ def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls"
+
+ /**
+ * Generate session key for the upload filename.
+ */
+ def Upload(fileId: String) = s"upload_${fileId}"
+
+ }
+
+ /**
+ * Define request keys.
+ */
+ object Request {
+
+ /**
+ * Request key for the Ajax request flag.
+ */
+ val Ajax = "AJAX"
+
+ /**
+ * Request key for the username which is used during Git repository access.
+ */
+ val UserName = "USER_NAME"
+
+ /**
+ * Generate request key for the request cache.
+ */
+ def Cache(key: String) = s"cache.${key}"
+
+ }
+
+}
diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala
new file mode 100644
index 0000000..4c6347c
--- /dev/null
+++ b/src/main/scala/util/LDAPUtil.scala
@@ -0,0 +1,104 @@
+package util
+
+import util.ControlUtil._
+import service.SystemSettingsService
+import com.novell.ldap._
+import service.SystemSettingsService.Ldap
+import scala.annotation.tailrec
+
+/**
+ * Utility for LDAP authentication.
+ */
+object LDAPUtil {
+
+ private val LDAP_VERSION: Int = 3
+
+ /**
+ * Try authentication by LDAP using given configuration.
+ * Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage).
+ */
+ def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = {
+ bind(
+ ldapSettings.host,
+ ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
+ ldapSettings.bindDN.getOrElse(""),
+ ldapSettings.bindPassword.getOrElse("")
+ ) match {
+ case Some(conn) => {
+ withConnection(conn) { conn =>
+ findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match {
+ case Some(userDN) => userAuthentication(ldapSettings, userDN, password)
+ case None => Left("User does not exist.")
+ }
+ }
+ }
+ case None => Left("System LDAP authentication failed.")
+ }
+ }
+
+ private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = {
+ bind(
+ ldapSettings.host,
+ ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort),
+ userDN,
+ password
+ ) match {
+ case Some(conn) => {
+ withConnection(conn) { conn =>
+ findMailAddress(conn, userDN, ldapSettings.mailAttribute) match {
+ case Some(mailAddress) => Right(mailAddress)
+ case None => Left("Can't find mail address.")
+ }
+ }
+ }
+ case None => Left("User LDAP Authentication Failed.")
+ }
+ }
+
+ private def bind(host: String, port: Int, dn: String, password: String): Option[LDAPConnection] = {
+ val conn: LDAPConnection = new LDAPConnection
+ try {
+ conn.connect(host, port)
+ conn.bind(LDAP_VERSION, dn, password.getBytes)
+ Some(conn)
+ } catch {
+ case e: Exception => {
+ if (conn.isConnected) conn.disconnect()
+ None
+ }
+ }
+ }
+
+ private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = {
+ try {
+ f(conn)
+ } finally {
+ conn.disconnect()
+ }
+ }
+
+ private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = {
+ @tailrec
+ def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = {
+ if(results.hasMore){
+ getEntries(results, entries :+ (try {
+ Option(results.next)
+ } catch {
+ case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD)
+ }))
+ } else {
+ entries.flatten
+ }
+ }
+ getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, userNameAttribute + "=" + userName, null, false)).collectFirst {
+ case x => x.getDN
+ }
+ }
+
+ private def findMailAddress(conn: LDAPConnection, userDN: String, mailAttribute: String): Option[String] =
+ defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](mailAttribute), false)){ results =>
+ optionIf (results.hasMore) {
+ Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue)
+ }
+ }
+}
diff --git a/src/main/scala/util/LockUtil.scala b/src/main/scala/util/LockUtil.scala
new file mode 100644
index 0000000..267b28b
--- /dev/null
+++ b/src/main/scala/util/LockUtil.scala
@@ -0,0 +1,36 @@
+package util
+
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.locks.{ReentrantLock, Lock}
+import util.ControlUtil._
+
+object LockUtil {
+
+ /**
+ * lock objects
+ */
+ private val locks = new ConcurrentHashMap[String, Lock]()
+
+ /**
+ * Returns the lock object for the specified repository.
+ */
+ private def getLockObject(key: String): Lock = synchronized {
+ if(!locks.containsKey(key)){
+ locks.put(key, new ReentrantLock())
+ }
+ locks.get(key)
+ }
+
+ /**
+ * Synchronizes a given function which modifies the working copy of the wiki repository.
+ */
+ def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock =>
+ try {
+ lock.lock()
+ f
+ } finally {
+ lock.unlock()
+ }
+ }
+
+}
diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala
new file mode 100644
index 0000000..4fc4652
--- /dev/null
+++ b/src/main/scala/util/Notifier.scala
@@ -0,0 +1,111 @@
+package util
+
+import scala.concurrent._
+import ExecutionContext.Implicits.global
+import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
+import org.slf4j.LoggerFactory
+
+import app.Context
+import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService}
+import servlet.Database
+import SystemSettingsService.Smtp
+
+trait Notifier extends RepositoryService with AccountService with IssuesService {
+ def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
+ (msg: String => String)(implicit context: Context): Unit
+
+ protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit context: Context) =
+ (
+ // individual repository's owner
+ issue.userName ::
+ // collaborators
+ getCollaborators(issue.userName, issue.repositoryName) :::
+ // participants
+ issue.openedUserName ::
+ getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
+ )
+ .distinct
+ .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded
+ .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) foreach (x => notify(x.mailAddress)) )
+
+}
+
+object Notifier {
+ // TODO We want to be able to switch to mock.
+ def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
+ case settings if settings.notification => new Mailer(settings.smtp.get)
+ case _ => new MockMailer
+ }
+
+ def msgIssue(url: String) = (content: String) => s"""
+ |${content}
+ |--
+ |View it on GitBucket
+ """.stripMargin
+
+ def msgPullRequest(url: String) = (content: String) => s"""
+ |${content}
+ |View, comment on, or merge it at:
+ |${url}
+ """.stripMargin
+
+ def msgComment(url: String) = (content: String) => s"""
+ |${content}
+ |--
+ |View it on GitBucket
+ """.stripMargin
+
+ def msgStatus(url: String) = (content: String) => s"""
+ |${content} #${url split('/') last}
+ """.stripMargin
+}
+
+class Mailer(private val smtp: Smtp) extends Notifier {
+ private val logger = LoggerFactory.getLogger(classOf[Mailer])
+
+ def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
+ (msg: String => String)(implicit context: Context) = {
+ val database = Database(context.request.getServletContext)
+
+ val f = future {
+ val email = new HtmlEmail
+ email.setHostName(smtp.host)
+ email.setSmtpPort(smtp.port.get)
+ smtp.user.foreach { user =>
+ email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
+ }
+ smtp.ssl.foreach { ssl =>
+ email.setSSLOnConnect(ssl)
+ }
+ smtp.fromAddress
+ .map (_ -> smtp.fromName.orNull)
+ .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
+ .foreach { case (address, name) =>
+ email.setFrom(address, name)
+ }
+ email.setHtmlMsg(msg(view.Markdown.toHtml(content, r, false, true)))
+
+ // TODO Can we use the Database Session in other than Transaction Filter?
+ database withSession {
+ getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
+ email.setSubject(s"[${r.name}] ${issue.title} (#${issueId})")
+ recipients(issue) {
+ email.getToAddresses.clear
+ email.addTo(_).send
+ }
+ }
+ }
+ "Notifications Successful."
+ }
+ f onSuccess {
+ case s => logger.debug(s)
+ }
+ f onFailure {
+ case t => logger.error("Notifications Failed.", t)
+ }
+ }
+}
+class MockMailer extends Notifier {
+ def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
+ (msg: String => String)(implicit context: Context): Unit = {}
+}
\ No newline at end of file
diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala
index ae6d868..61c5c18 100644
--- a/src/main/scala/util/StringUtil.scala
+++ b/src/main/scala/util/StringUtil.scala
@@ -1,14 +1,16 @@
package util
import java.net.{URLDecoder, URLEncoder}
+import org.mozilla.universalchardet.UniversalDetector
+import util.ControlUtil._
object StringUtil {
- def sha1(value: String): String = {
- val md = java.security.MessageDigest.getInstance("SHA-1")
- md.update(value.getBytes)
- md.digest.map(b => "%02x".format(b)).mkString
- }
+ def sha1(value: String): String =
+ defining(java.security.MessageDigest.getInstance("SHA-1")){ md =>
+ md.update(value.getBytes)
+ md.digest.map(b => "%02x".format(b)).mkString
+ }
def md5(value: String): String = {
val md = java.security.MessageDigest.getInstance("MD5")
@@ -20,4 +22,20 @@
def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8")
+ def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
+
+ def escapeHtml(value: String): String =
+ value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
+
+ def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content))
+
+ def detectEncoding(content: Array[Byte]): String =
+ defining(new UniversalDetector(null)){ detector =>
+ detector.handleData(content, 0, content.length)
+ detector.dataEnd()
+ detector.getDetectedCharset match {
+ case null => "UTF-8"
+ case e => e
+ }
+ }
}
diff --git a/src/main/scala/util/Validations.scala b/src/main/scala/util/Validations.scala
index 1d42d99..3b71280 100644
--- a/src/main/scala/util/Validations.scala
+++ b/src/main/scala/util/Validations.scala
@@ -1,7 +1,6 @@
package util
import jp.sf.amateras.scalatra.forms._
-import scala.Some
trait Validations {
@@ -9,7 +8,7 @@
* Constraint for the identifier such as user name, repository name or page name.
*/
def identifier: Constraint = new Constraint(){
- def validate(name: String, value: String): Option[String] =
+ override def validate(name: String, value: String): Option[String] =
if(!value.matches("^[a-zA-Z0-9\\-_]+$")){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
@@ -26,10 +25,7 @@
*/
def date(constraints: Constraint*): SingleValueType[java.util.Date] =
new SingleValueType[java.util.Date]((pattern("\\d{4}-\\d{2}-\\d{2}") +: constraints): _*){
- def convert(value: String): java.util.Date = {
- val formatter = new java.text.SimpleDateFormat("yyyy-MM-dd")
- formatter.parse(value)
- }
+ def convert(value: String): java.util.Date = new java.text.SimpleDateFormat("yyyy-MM-dd").parse(value)
}
}
diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala
index c2c7fa2..c5ae04e 100644
--- a/src/main/scala/view/AvatarImageProvider.scala
+++ b/src/main/scala/view/AvatarImageProvider.scala
@@ -12,17 +12,23 @@
*/
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
- val src = getAccountByUserName(userName).collect { case account if(account.image.isEmpty) =>
- s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
+
+ val src = getAccountByUserName(userName).map { account =>
+ if(account.image.isEmpty && getSystemSettings().gravatar){
+ s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
+ } else {
+ s"""${context.path}/${userName}/_avatar"""
+ }
} getOrElse {
- if(mailAddress.nonEmpty){
+ if(mailAddress.nonEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
} else {
s"""${context.path}/${userName}/_avatar"""
}
}
+
if(tooltip){
- Html(s"""
""")
+ Html(s"""
""")
} else {
Html(s"""
""")
}
diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala
index 32cd513..90da6da 100644
--- a/src/main/scala/view/Markdown.scala
+++ b/src/main/scala/view/Markdown.scala
@@ -1,12 +1,14 @@
package view
import util.StringUtil
+import util.ControlUtil._
+import util.Directory._
import org.parboiled.common.StringUtils
import org.pegdown._
import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering
import scala.collection.JavaConverters._
-import service.RequestCache
+import service.{RequestCache, WikiService}
object Markdown {
@@ -29,7 +31,7 @@
}
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
- enableWikiLink: Boolean) extends LinkRenderer {
+ enableWikiLink: Boolean) extends LinkRenderer with WikiService {
override def render(node: WikiLinkNode): Rendering = {
if(enableWikiLink){
try {
@@ -40,8 +42,14 @@
} else {
(text, text)
}
+
val url = repository.url.replaceFirst("/git/", "/").replaceFirst("\\.git$", "") + "/wiki/" + StringUtil.urlEncode(page)
- new Rendering(url, label)
+
+ if(getWikiPage(repository.owner, repository.name, page).isDefined){
+ new Rendering(url, label)
+ } else {
+ new Rendering(url, label).withAttribute("class", "absent")
+ }
} catch {
case e: java.io.UnsupportedEncodingException => throw new IllegalStateException
}
@@ -52,7 +60,7 @@
}
class GitBucketVerbatimSerializer extends VerbatimSerializer {
- def serialize(node: VerbatimNode, printer: Printer) {
+ def serialize(node: VerbatimNode, printer: Printer): Unit = {
printer.println.print(" length){
+ value.substring(0, length) + "..."
+ } else {
+ value
+ }
+
+ import scala.util.matching.Regex
+ import scala.util.matching.Regex._
+ implicit class RegexReplaceString(s: String) {
+ def replaceAll(pattern: String, replacer: (Match) => String): String = {
+ pattern.r.replaceAllIn(s, replacer)
+ }
+ }
+
def activityMessage(message: String)(implicit context: app.Context): Html =
Html(message
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""")
+ .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""")
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""$$1/$$2""")
- .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""$$3""")
- .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""$$3""")
+ .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", (m: Match) => s"""${m.group(3)}""")
+ .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""${m.group(3)}""")
.replaceAll("\\[user:([^\\s]+?)\\]" , s"""$$1""")
)
+ /**
+ * URL encode except '/'.
+ */
+ def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/")
+
def urlEncode(value: String): String = StringUtil.urlEncode(value)
def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")
@@ -73,14 +93,12 @@
/**
* Generates the url to the account page.
*/
- def url(userName: String)(implicit context: app.Context): String =
- s"${context.path}/${userName}"
+ def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}"
/**
* Returns the url to the root of assets.
*/
- def assets(implicit context: app.Context): String =
- s"${context.path}/assets"
+ def assets(implicit context: app.Context): String = s"${context.path}/assets"
/**
* Generates the link to the account page.
@@ -91,6 +109,8 @@
} getOrElse Html(userName)
}
+ def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime
+
/**
* Implicit conversion to add mkHtml() to Seq[Html].
*/
diff --git a/src/main/twirl/account/activity.scala.html b/src/main/twirl/account/activity.scala.html
index 29b2982..1f2cefc 100644
--- a/src/main/twirl/account/activity.scala.html
+++ b/src/main/twirl/account/activity.scala.html
@@ -1,23 +1,6 @@
-@(account: model.Account, activities: List[model.Activity])(implicit context: app.Context)
+@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
-@html.main(account.userName){
-
-
-
-
-
@avatar(account.userName, 200)
-
-
-
-
-
Joined on @date(account.registeredDate)
-
-
-
- @tab(account, "activity")
- @helper.html.activities(activities)
-
-
-
+@main(account, groupNames, "activity"){
+ @helper.html.activities(activities)
}
diff --git a/src/main/twirl/account/edit.scala.html b/src/main/twirl/account/edit.scala.html
index 2eeb22a..8d8ca9a 100644
--- a/src/main/twirl/account/edit.scala.html
+++ b/src/main/twirl/account/edit.scala.html
@@ -13,34 +13,43 @@
@if(account.isEmpty){
}
+ @if(account.map(_.password.nonEmpty).getOrElse(true)){
+
+ }
diff --git a/src/main/twirl/account/main.scala.html b/src/main/twirl/account/main.scala.html
new file mode 100644
index 0000000..0b6cc6e
--- /dev/null
+++ b/src/main/twirl/account/main.scala.html
@@ -0,0 +1,49 @@
+@(account: model.Account, groupNames: List[String], active: String)(body: Html)(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main(account.userName){
+
+
+
+
+
@avatar(account.userName, 200)
+
@account.fullName
+
@account.userName
+
+
+ @if(account.url.isDefined){
+
+ }
+
Joined on @date(account.registeredDate)
+
+ @if(groupNames.nonEmpty){
+
+ }
+
+
+
+
+ - Repositories
+ @if(account.isGroupAccount){
+ - Members
+ } else {
+ - Public Activity
+ }
+ @if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
+ -
+
+
+ }
+
+ @body
+
+
+
+}
diff --git a/src/main/twirl/account/members.scala.html b/src/main/twirl/account/members.scala.html
new file mode 100644
index 0000000..14d7c77
--- /dev/null
+++ b/src/main/twirl/account/members.scala.html
@@ -0,0 +1,16 @@
+@(account: model.Account, members: List[String])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@main(account, Nil, "members"){
+ @if(members.isEmpty){
+ No members
+ } else {
+ @members.map { userName =>
+
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html
index 9a2ce34..f9037f5 100644
--- a/src/main/twirl/account/repositories.scala.html
+++ b/src/main/twirl/account/repositories.scala.html
@@ -1,42 +1,26 @@
-@(account: model.Account, repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
+@(account: model.Account, groupNames: List[String], repositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context)
@import context._
@import view.helpers._
-@html.main(account.userName){
-
-
-
-
-
@avatar(account.userName, 200)
-
-
-
-
-
Joined on @date(account.registeredDate)
-
-
-
- @tab(account, "repositories")
- @if(repositories.isEmpty){
- No repositories
- } else {
- @repositories.map { repository =>
-
-
- @if(repository.repository.description.isDefined){
-
@repository.repository.description
- }
-
Last updated: @datetime(repository.repository.lastActivityDate)
-
+@main(account, groupNames, "repositories"){
+ @if(repositories.isEmpty){
+ No repositories
+ } else {
+ @repositories.map { repository =>
+
+
+ @if(repository.repository.originUserName.isDefined){
+
}
+ @if(repository.repository.description.isDefined){
+
@repository.repository.description
+ }
+
Last updated: @datetime(repository.repository.lastActivityDate)
-
-
+ }
+ }
}
diff --git a/src/main/twirl/account/tab.scala.html b/src/main/twirl/account/tab.scala.html
deleted file mode 100644
index 2a12a0c..0000000
--- a/src/main/twirl/account/tab.scala.html
+++ /dev/null
@@ -1,14 +0,0 @@
-@(account: model.Account, active: String)(implicit context: app.Context)
-@import context._
-@import view.helpers._
-
diff --git a/src/main/twirl/admin/menu.scala.html b/src/main/twirl/admin/menu.scala.html
index d9c7969..8e366a7 100644
--- a/src/main/twirl/admin/menu.scala.html
+++ b/src/main/twirl/admin/menu.scala.html
@@ -10,6 +10,9 @@
System Settings
+
+ H2 Console
+
diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html
index 75a07b6..361138d 100644
--- a/src/main/twirl/admin/system.scala.html
+++ b/src/main/twirl/admin/system.scala.html
@@ -8,17 +8,151 @@
}
-}
\ No newline at end of file
+}
+
\ No newline at end of file
diff --git a/src/main/twirl/admin/users/edit.scala.html b/src/main/twirl/admin/users/edit.scala.html
deleted file mode 100644
index b6d3213..0000000
--- a/src/main/twirl/admin/users/edit.scala.html
+++ /dev/null
@@ -1,55 +0,0 @@
-@(account: Option[model.Account])(implicit context: app.Context)
-@import context._
-@html.main(if(account.isEmpty) "New User" else "Update User"){
- @admin.html.menu("users"){
-
- }
-}
\ No newline at end of file
diff --git a/src/main/twirl/admin/users/group.scala.html b/src/main/twirl/admin/users/group.scala.html
new file mode 100644
index 0000000..240622c
--- /dev/null
+++ b/src/main/twirl/admin/users/group.scala.html
@@ -0,0 +1,116 @@
+@(account: Option[model.Account], members: List[String])(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main(if(account.isEmpty) "New Group" else "Update Group"){
+ @admin.html.menu("users"){
+
+ }
+}
+
\ No newline at end of file
diff --git a/src/main/twirl/admin/users/list.scala.html b/src/main/twirl/admin/users/list.scala.html
index abe96f4..df75de3 100644
--- a/src/main/twirl/admin/users/list.scala.html
+++ b/src/main/twirl/admin/users/list.scala.html
@@ -1,30 +1,46 @@
-@(users: List[model.Account])(implicit context: app.Context)
+@(users: List[model.Account], members: Map[String, List[String]])(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main("Manage Users"){
@admin.html.menu("users"){
@users.map { account =>
- Edit
+ @if(account.isGroupAccount){
+ Edit
+ } else {
+ Edit
+ }
@avatar(account.userName, 20)
@account.userName
- @if(account.isAdmin){
- (Administrator)
+ @if(account.isGroupAccount){
+ (Group)
} else {
- (Normal)
+ @if(account.isAdmin){
+ (Administrator)
+ } else {
+ (Normal)
+ }
+ }
+ @if(account.isGroupAccount){
+ @members(account.userName).map { userName =>
+ @avatar(userName, 20, tooltip = true)
+ }
}
- @account.mailAddress
+ @if(!account.isGroupAccount){
+ @account.mailAddress
+ }
@account.url.map { url =>
@url
}
@@ -32,7 +48,9 @@
Registered: @datetime(account.registeredDate)
Updated: @datetime(account.updatedDate)
- Last Login: @account.lastLoginDate.map(datetime)
+ @if(!account.isGroupAccount){
+ Last Login: @account.lastLoginDate.map(datetime)
+ }
|
diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html
new file mode 100644
index 0000000..b49d710
--- /dev/null
+++ b/src/main/twirl/admin/users/user.scala.html
@@ -0,0 +1,74 @@
+@(account: Option[model.Account])(implicit context: app.Context)
+@import context._
+@html.main(if(account.isEmpty) "New User" else "Update User"){
+ @admin.html.menu("users"){
+
+ }
+}
diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html
new file mode 100644
index 0000000..9ebdede
--- /dev/null
+++ b/src/main/twirl/dashboard/issues.scala.html
@@ -0,0 +1,48 @@
+@(listparts: twirl.api.Html,
+ allCount: Int,
+ assignedCount: Int,
+ createdByCount: Int,
+ repositories: List[(String, String, Int)],
+ condition: service.IssuesService.IssueSearchCondition,
+ filter: String)(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main("Your Issues"){
+@dashboard.html.tab("issues")
+
+}
diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html
new file mode 100644
index 0000000..4ffc18f
--- /dev/null
+++ b/src/main/twirl/dashboard/pulls.scala.html
@@ -0,0 +1,40 @@
+@(listparts: twirl.api.Html,
+ counts: List[service.PullRequestService.PullRequestCount],
+ repositories: List[(String, String, Int)],
+ condition: service.IssuesService.IssueSearchCondition,
+ filter: String)(implicit context: app.Context)
+@import context._
+@import view.helpers._
+@html.main("Your Issues"){
+@dashboard.html.tab("pulls")
+
+}
diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html
new file mode 100644
index 0000000..21e4d06
--- /dev/null
+++ b/src/main/twirl/dashboard/tab.scala.html
@@ -0,0 +1,9 @@
+@(active: String = "")(implicit context: app.Context)
+@import context._
+
diff --git a/src/main/twirl/header.scala.html b/src/main/twirl/header.scala.html
index 45b939d..b132711 100644
--- a/src/main/twirl/header.scala.html
+++ b/src/main/twirl/header.scala.html
@@ -1,10 +1,30 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
+
+@if(repository.commitCount > 0){
+
+}
diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html
index 6b1662c..15c8cdc 100644
--- a/src/main/twirl/helper/activities.scala.html
+++ b/src/main/twirl/helper/activities.scala.html
@@ -1,43 +1,96 @@
@(activities: List[model.Activity])(implicit context: app.Context)
@import context._
@import view.helpers._
+
@if(activities.isEmpty){
No activity
} else {
@activities.map { activity =>
-
@datetime(activity.activityDate)
-
- @avatar(activity.activityUserName, 16)
- @activityMessage(activity.message)
-
- @activity.additionalInfo.map { additionalInfo =>
- @(activity.activityType match {
- case "create_wiki" => {
-
- }
- case "edit_wiki" => {
-
- }
- case "push" => {
-
- {additionalInfo.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
- if(i == 3){
-
...
- } else {
+ @(activity.activityType match {
+ case "open_issue" => detailActivity(activity, "activity-issue.png")
+ case "comment_issue" => detailActivity(activity, "activity-comment.png")
+ case "close_issue" => detailActivity(activity, "activity-issue-close.png")
+ case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
+ case "open_pullreq" => detailActivity(activity, "activity-merge.png")
+ case "merge_pullreq" => detailActivity(activity, "activity-merge.png")
+ case "create_repository" => simpleActivity(activity, "activity-create-repository.png")
+ case "create_branch" => simpleActivity(activity, "activity-branch.png")
+ case "create_tag" => simpleActivity(activity, "activity-tag.png")
+ case "fork" => simpleActivity(activity, "activity-fork.png")
+ case "push" => customActivity(activity, "activity-commit.png"){
+
+ {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) =>
+ if(i == 3){
+
...
+ } else {
+ if(commit.nonEmpty){
}
- }}
-
+ }
+ }}
+
+ }
+ case "create_wiki" => customActivity(activity, "activity-wiki.png"){
+
+ }
+ case "edit_wiki" => customActivity(activity, "activity-wiki.png"){
+ activity.additionalInfo.get.split(":") match {
+ case Array(pageName, commitId) =>
+
+ case Array(pageName) =>
+
}
- case _ => {
-
{additionalInfo}
- }
- })
- }
+ }
+ })
}
}
+
+@detailActivity(activity: model.Activity, image: String) = {
+ 
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+
+ @activity.additionalInfo.map { additionalInfo =>
+
@additionalInfo
+ }
+
+}
+
+@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
+
+
+
@datetime(activity.activityDate)
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+
+ @additionalInfo
+
+}
+
+@simpleActivity(activity: model.Activity, image: String) = {
+
+
+
+ @avatar(activity.activityUserName, 16)
+ @activityMessage(activity.message)
+ @datetime(activity.activityDate)
+
+
+}
+
diff --git a/src/main/twirl/helper/copy.scala.html b/src/main/twirl/helper/copy.scala.html
new file mode 100644
index 0000000..1cba6e6
--- /dev/null
+++ b/src/main/twirl/helper/copy.scala.html
@@ -0,0 +1,36 @@
+@(id: String, value: String)(html: Html)
+
+ @html
+
+
+
\ No newline at end of file
diff --git a/src/main/twirl/helper/diff.scala.html b/src/main/twirl/helper/diff.scala.html
index 0360230..9e517ab 100644
--- a/src/main/twirl/helper/diff.scala.html
+++ b/src/main/twirl/helper/diff.scala.html
@@ -1,7 +1,39 @@
-@(diffs: Seq[util.JGitUtil.DiffInfo], repository: service.RepositoryService.RepositoryInfo, commitId: Option[String])(implicit context: app.Context)
+@(diffs: Seq[util.JGitUtil.DiffInfo],
+ repository: service.RepositoryService.RepositoryInfo,
+ newCommitId: Option[String],
+ oldCommitId: Option[String],
+ showIndex: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
+@if(showIndex){
+
+
+
+
+ Showing @diffs.size changed @plural(diffs.size, "file")
+
+
+}
@diffs.zipWithIndex.map { case (diff, i) =>
@@ -9,17 +41,27 @@
@@ -90,6 +132,17 @@
}
$(function(){
+ @if(showIndex){
+ $('#toggle-file-list').click(function(){
+ $('#commit-file-list').toggle();
+ if($(this).val() == 'Show file list'){
+ $(this).val('Hide file list');
+ } else {
+ $(this).val('Show file list');
+ }
+ });
+ }
+
@diffs.zipWithIndex.map { case (diff, i) =>
@if(diff.newContent != None || diff.oldContent != None){
if($('#oldText-@i').length > 0){
diff --git a/src/main/twirl/helper/dropdown.scala.html b/src/main/twirl/helper/dropdown.scala.html
index 6c86e96..f7479a8 100644
--- a/src/main/twirl/helper/dropdown.scala.html
+++ b/src/main/twirl/helper/dropdown.scala.html
@@ -1,10 +1,13 @@
-@(buttonValue: String = "")(body: Html)
-
-