diff --git a/README.md b/README.md
index e56432c..2ab7d07 100644
--- a/README.md
+++ b/README.md
@@ -7,19 +7,20 @@
- Public / Private Git repository (http access only)
- Repository viewer (some advanced features are not implemented)
+- Repository search (Code and Issues)
- Wiki
- Issues
- Activity timeline
- User management (for Administrators)
+- Group (like Organization in Github)
Following features are not implemented, but we will make them in the future release!
- Fork and pull request
-- Search
- Network graph
- Statics
- Watch / Star
-- Team management (like Organization in Github)
+- Notification
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
@@ -36,6 +37,16 @@
Release Notes
--------
+### 1.4 - 31 Jul 2013
+- Group management.
+- Repository search for code and issues.
+- Display user related issues on the dashboard.
+- Display participants avatar of issues on the issue page.
+- Performance improvement for repository viewer.
+- Alert by milestone due date.
+- H2 database administration console.
+- Fixed some bugs.
+
### 1.3 - 18 Jul 2013
- Batch updating for issues.
- Display assigned user on issue list.
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..785ffa6
--- /dev/null
+++ b/etc/icons.svg
@@ -0,0 +1,222 @@
+
+
+
+
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.scala b/project/build.scala
index 774c0dd..cc8251e 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
@@ -20,7 +21,10 @@
name := Name,
version := Version,
scalaVersion := ScalaVersion,
- resolvers += Classpaths.typesafeReleases,
+ resolvers ++= Seq(
+ Classpaths.typesafeReleases,
+ "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"
+ ),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r",
"org.apache.commons" % "commons-io" % "1.3.2",
@@ -28,6 +32,7 @@
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.4",
+ "jp.sf.amateras" %% "scalatra-forms" % "0.0.1",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.3.0",
"org.apache.commons" % "commons-compress" % "1.5",
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 3249832..e75b1d5 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,6 +1,6 @@
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.2")
-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")
diff --git a/src/main/resources/update/1_4.sql b/src/main/resources/update/1_4.sql
index a68ca43..2d3c492 100644
--- a/src/main/resources/update/1_4.sql
+++ b/src/main/resources/update/1_4.sql
@@ -8,3 +8,17 @@
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..35131fc
--- /dev/null
+++ b/src/main/resources/update/1_5.sql
@@ -0,0 +1,22 @@
+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 PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK1 FOREIGN KEY (REQUEST_USER_NAME, REQUEST_REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
+
+ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index 1233a28..874bbbd 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -18,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 acf066f..48b649b 100644
--- a/src/main/scala/app/AccountController.scala
+++ b/src/main/scala/app/AccountController.scala
@@ -58,7 +58,7 @@
case _ =>
_root_.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
- getVisibleRepositories(userName, baseUrl, context.loginAccount.map(_.userName)))
+ getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)))
}
} getOrElse NotFound
}
diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala
index bff5994..c359ff2 100644
--- a/src/main/scala/app/ControllerBase.scala
+++ b/src/main/scala/app/ControllerBase.scala
@@ -1,7 +1,7 @@
package app
import _root_.util.Directory._
-import _root_.util.{FileUtil, Validations}
+import _root_.util.{StringUtil, FileUtil, Validations}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
@@ -10,7 +10,7 @@
import model.Account
import scala.Some
import service.AccountService
-import javax.servlet.http.{HttpSession, HttpServletRequest}
+import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest}
import java.text.SimpleDateFormat
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
@@ -23,16 +23,28 @@
implicit val jsonFormats = DefaultFormats
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
- val httpRequest = request.asInstanceOf[HttpServletRequest]
- val path = httpRequest.getRequestURI.substring(request.getServletContext.getContextPath.length)
+ 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/")){
- Option(httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").asInstanceOf[Account]).collect {
- case account if(account.isAdmin) => chain.doFilter(request, response)
+ val account = httpRequest.getSession.getAttribute("LOGIN_ACCOUNT").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)
}
}
diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala
index a622c88..3446d16 100644
--- a/src/main/scala/app/CreateRepositoryController.scala
+++ b/src/main/scala/app/CreateRepositoryController.scala
@@ -1,27 +1,30 @@
package app
import util.Directory._
-import util.{JGitUtil, UsersAuthenticator}
+import util.{LockUtil, JGitUtil, UsersAuthenticator, ReferrerAuthenticator}
import service._
import java.io.File
import org.eclipse.jgit.api.Git
import org.apache.commons.io._
import jp.sf.amateras.scalatra.forms._
+import org.eclipse.jgit.lib.PersonIdent
class CreateRepositoryController extends CreateRepositoryControllerBase
with RepositoryService with AccountService with WikiService with LabelsService with ActivityService
- with UsersAuthenticator
+ with UsersAuthenticator with ReferrerAuthenticator
/**
* Creates new repository.
*/
trait CreateRepositoryControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService
- with UsersAuthenticator =>
+ with UsersAuthenticator with ReferrerAuthenticator =>
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()))),
@@ -29,6 +32,11 @@
"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.
*/
@@ -39,77 +47,142 @@
/**
* Create new repository.
*/
- post("/new", form)(usersOnly { form =>
- val ownerAccount = getAccountByUserName(form.owner).get
- 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, form.owner, form.description, form.isPrivate)
+ // Insert to the database at first
+ createRepository(form.name, form.owner, form.description, form.isPrivate)
- // Add collaborators for group repository
- if(ownerAccount.isGroupAccount){
- getGroupMembers(form.owner).foreach { userName =>
- addCollaborator(form.owner, form.name, userName)
+ // Add collaborators for group repository
+ if(ownerAccount.isGroupAccount){
+ getGroupMembers(form.owner).foreach { userName =>
+ addCollaborator(form.owner, form.name, userName)
+ }
+ }
+
+ // Insert default labels
+ insertDefaultLabels(loginUserName, form.name)
+
+ // Create the actual repository
+ val gitdir = getRepositoryDir(form.owner, form.name)
+ JGitUtil.initRepository(gitdir)
+
+ if(form.createReadme){
+ val tmpdir = getInitRepositoryDir(form.owner, form.name)
+ try {
+ // Clone the repository
+ Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
+
+ // Create README.md
+ FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
+ if(form.description.nonEmpty){
+ form.name + "\n" +
+ "===============\n" +
+ "\n" +
+ form.description.get
+ } else {
+ form.name + "\n" +
+ "===============\n"
+ }, "UTF-8")
+
+ val git = Git.open(tmpdir)
+ git.add.addFilepattern("README.md").call
+ git.commit
+ .setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress))
+ .setMessage("Initial commit").call
+ git.push.call
+
+ } finally {
+ FileUtils.deleteDirectory(tmpdir)
+ }
+ }
+
+ // 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}")
}
-
- // Insert default labels
- createLabel(form.owner, form.name, "bug", "fc2929")
- createLabel(form.owner, form.name, "duplicate", "cccccc")
- createLabel(form.owner, form.name, "enhancement", "84b6eb")
- createLabel(form.owner, form.name, "invalid", "e6e6e6")
- createLabel(form.owner, form.name, "question", "cc317c")
- createLabel(form.owner, form.name, "wontfix", "ffffff")
-
- // Create the actual repository
- val gitdir = getRepositoryDir(form.owner, form.name)
- JGitUtil.initRepository(gitdir)
-
- if(form.createReadme){
- val tmpdir = getInitRepositoryDir(form.owner, form.name)
- try {
- // Clone the repository
- Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call
-
- // Create README.md
- FileUtils.writeStringToFile(new File(tmpdir, "README.md"),
- if(form.description.nonEmpty){
- form.name + "\n" +
- "===============\n" +
- "\n" +
- form.description.get
- } else {
- form.name + "\n" +
- "===============\n"
- }, "UTF-8")
-
- val git = Git.open(tmpdir)
- git.add.addFilepattern("README.md").call
- git.commit.setMessage("Initial commit").call
- git.push.call
-
- } finally {
- FileUtils.deleteDirectory(tmpdir)
- }
- }
-
- // 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}")
})
+ post("/:owner/:repository/_fork")(referrersOnly { 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
+ JGitUtil.withGit(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(){
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.
*/
@@ -120,4 +193,4 @@
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala
index 02e1668..2728b5e 100644
--- a/src/main/scala/app/DashboardController.scala
+++ b/src/main/scala/app/DashboardController.scala
@@ -33,17 +33,25 @@
session.put(sessionKey, condition)
- val repositories = getAccessibleRepositories(context.loginAccount, baseUrl)
+ 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(Nil, 0, 0, 0, condition),
- 0,
- 0,
- 0,
- repositories,
+ 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, repositories: _*),
condition,
filter)
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala
index 5612f9b..f827492 100644
--- a/src/main/scala/app/IndexController.scala
+++ b/src/main/scala/app/IndexController.scala
@@ -16,19 +16,22 @@
val loginAccount = context.loginAccount
html.index(getRecentActivities(),
- getAccessibleRepositories(loginAccount, baseUrl),
+ getVisibleRepositories(loginAccount, baseUrl),
loadSystemSettings(),
- loginAccount.map{ account => getRepositoryNamesOfUser(account.userName) }.getOrElse(Nil)
+ loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil)
)
}
/**
* JSON API for collaborator completion.
+ *
+ * TODO Move to other controller?
*/
- // 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))
+ 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 0c8b35f..ccebc5e 100644
--- a/src/main/scala/app/IssuesController.scala
+++ b/src/main/scala/app/IssuesController.scala
@@ -128,14 +128,22 @@
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, Some(form.content), repository)() map { id =>
- redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
+ handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
+ if(issue.isPullRequest){
+ redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
+ } else {
+ redirect(s"/${repository.owner}/${repository.name}/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"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
+ handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
+ if(issue.isPullRequest){
+ redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}")
+ } else {
+ redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}")
+ }
} getOrElse NotFound
})
@@ -294,23 +302,17 @@
content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) )
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
- commentId
+ (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 filterUser = Map(filter -> params.getOrElse("userName", ""))
+ val page = IssueSearchCondition.page(request)
val sessionKey = s"${owner}/${repoName}/issues"
- 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 = if(request.getQueryString == null){
session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
@@ -319,17 +321,17 @@
session.put(sessionKey, condition)
issues.html.list(
- searchIssue(owner, repoName, condition, filter, userName, (page - 1) * IssueLimit, IssueLimit),
+ searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName),
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),
+ 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,
diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala
new file mode 100644
index 0000000..d361d44
--- /dev/null
+++ b/src/main/scala/app/PullRequestsController.scala
@@ -0,0 +1,400 @@
+package app
+
+import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator}
+import util.Directory._
+import util.Implicits._
+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 scala.Some
+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 =>
+ val owner = repository.owner
+ val name = repository.name
+ val issueId = params("id").toInt
+
+ getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
+ JGitUtil.withGit(getRepositoryDir(owner, name)){ git =>
+ val requestCommitId = git.getRepository.resolve(pullreq.requestBranch)
+
+ val (commits, diffs) =
+ getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
+
+ pulls.html.pullreq(
+ issue, pullreq,
+ getComments(owner, name, issueId.toInt),
+ (getCollaborators(owner, name) :+ owner).sorted,
+ getMilestonesWithIssueCount(owner, name),
+ commits,
+ diffs,
+ requestCommitId.getName,
+ if(issue.closed){
+ false
+ } else {
+ checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch)
+ },
+ hasWritePermission(owner, name, context.loginAccount),
+ repository,
+ s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
+ }
+
+ } getOrElse NotFound
+ })
+
+ post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
+ LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){
+ val issueId = params("id").toInt
+
+ getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
+ val remote = getRepositoryDir(repository.owner, repository.name)
+ val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
+ val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call
+
+ try {
+ // mark issue as merged and close.
+ val loginAccount = context.loginAccount.get
+ createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Merge", "merge")
+ createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
+ updateClosed(repository.owner, repository.name, issueId, true)
+ recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)
+
+ // fetch pull request to working repository
+ val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}"
+
+ git.fetch
+ .setRemote(getRepositoryDir(repository.owner, repository.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.userName, loginAccount.mailAddress))
+ .call
+
+ // push
+ git.push.call
+
+ val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom,
+ pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
+
+ commits.flatten.foreach { commit =>
+ if(!existsCommitId(repository.owner, repository.name, commit.id)){
+ insertCommitId(repository.owner, repository.name, commit.id)
+ }
+ }
+
+ redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
+
+ } finally {
+ git.getRepository.close
+ FileUtils.deleteDirectory(tmpdir)
+ }
+ } getOrElse NotFound
+ }
+ })
+
+ /**
+ * 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)
+ val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check")
+ if(tmpdir.exists()){
+ FileUtils.deleteDirectory(tmpdir)
+ }
+
+ val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call
+ try {
+ 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
+
+ } finally {
+ git.getRepository.close
+ FileUtils.deleteDirectory(tmpdir)
+ }
+ }
+ }
+
+ get("/:owner/:repository/compare")(collaboratorsOnly { forkedRepository =>
+ (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
+ case (Some(originUserName), Some(originRepositoryName)) => {
+ getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository =>
+ withGit(
+ getRepositoryDir(originUserName, originRepositoryName),
+ 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 _ => {
+ JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git =>
+ val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2
+ redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
+ }
+ }
+ }
+ })
+
+ get("/:owner/:repository/compare/*...*")(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)) => {
+ withGit(
+ getRepositoryDir(originOwner, repository.name),
+ 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 =>
+ getRepositoryNames(getForkedRepositoryTree(userName, repository.name))
+ } getOrElse Nil,
+ originBranch,
+ forkedBranch,
+ oldId.getName,
+ newId.getName,
+ checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch),
+ repository,
+ originRepository,
+ forkedRepository)
+ }
+ }
+ 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
+ JGitUtil.withGit(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
+ }
+
+ recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
+
+ redirect(s"/${repository.owner}/${repository.name}/pulls/${issueId}")
+ })
+
+ /**
+ * Handles w Git object simultaneously.
+ */
+ private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
+ val oldGit = Git.open(oldDir)
+ val newGit = Git.open(newDir)
+ try {
+ action(oldGit, newGit)
+ } finally {
+ oldGit.getRepository.close
+ newGit.getRepository.close
+ }
+ }
+
+ /**
+ * 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]) = {
+
+ withGit(
+ getRepositoryDir(userName, repositoryName),
+ 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) = {
+ val owner = repository.owner
+ val repoName = repository.name
+ val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
+ val page = IssueSearchCondition.page(request)
+ val sessionKey = s"${owner}/${repoName}/pulls"
+
+ // retrieve search condition
+ val condition = if(request.getQueryString == null){
+ session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition]
+ } else IssueSearchCondition(request)
+
+ session.put(sessionKey, condition)
+
+ pulls.html.list(
+ searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
+ getPullRequestCount(condition.state == "closed", Some(owner, 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 780a7f3..14c4f91 100644
--- a/src/main/scala/app/RepositorySettingsController.scala
+++ b/src/main/scala/app/RepositorySettingsController.scala
@@ -45,7 +45,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")
})
diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala
index 43cb126..f5e7dc7 100644
--- a/src/main/scala/app/RepositoryViewerController.scala
+++ b/src/main/scala/app/RepositoryViewerController.scala
@@ -38,48 +38,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.getOrElse("page", "1").toInt
JGitUtil.withGit(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,10 +71,9 @@
/**
* 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 =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@@ -202,7 +181,25 @@
BadRequest
}
})
+
+ get("/:owner/:repository/network/members")(referrersOnly { repository =>
+ repo.html.forked(
+ getForkedRepositoryTree(
+ 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) get
+
+ (id, path.substring(id.length).replaceFirst("^/", ""))
+ }
+
/**
* Provides HTML of the file list.
*
@@ -218,7 +215,7 @@
JGitUtil.withGit(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) =>
+ JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
val revCommit = JGitUtil.getRevCommitFromId(git, objectId)
// get files
diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala
index 38bf35a..7a89006 100644
--- a/src/main/scala/app/SearchController.scala
+++ b/src/main/scala/app/SearchController.scala
@@ -22,7 +22,7 @@
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)}")
+ redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala
index 932f9e2..c1c1feb 100644
--- a/src/main/scala/app/WikiController.scala
+++ b/src/main/scala/app/WikiController.scala
@@ -59,7 +59,7 @@
val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
- wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository)
+ wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
}
})
@@ -67,7 +67,7 @@
val commitId = params("commitId").split("\\.\\.\\.")
JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git =>
- wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository)
+ wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository)
}
})
@@ -105,9 +105,10 @@
})
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository =>
- val pageName = StringUtil.urlDecode(params("page"))
+ val pageName = StringUtil.urlDecode(params("page"))
+ val account = context.loginAccount.get
- deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}")
+ deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}")
updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki")
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/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala
index 0b7e261..389be4a 100644
--- a/src/main/scala/service/ActivityService.scala
+++ b/src/main/scala/service/ActivityService.scala
@@ -102,7 +102,28 @@
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)
}
diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala
index 25eae21..46706d0 100644
--- a/src/main/scala/service/IssuesService.scala
+++ b/src/main/scala/service/IssuesService.scala
@@ -42,18 +42,18 @@
/**
* 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 = {
+ def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
+ repos: (String, String)*): Int = {
// TODO It must be _.length instead of map (_.issueId) list).length.
// But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
// https://github.com/slick/slick/issues/170
- (searchIssueQuery(owner, repository, condition, filter, userName) map (_.issueId) list).length
+ (searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length
}
/**
* Returns the Map which contains issue count for each labels.
@@ -61,14 +61,13 @@
* @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 +82,97 @@
}
.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 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], repos: (String, String)*): List[(String, String, Int)] = {
+ searchIssueQuery(repos, condition.copy(repo = None), filterUser, false)
+ .groupBy { t =>
+ t.userName ~ t.repositoryName
+ }
+ .map { case (repo, t) =>
+ repo ~ t.length
+ }
+ .filter (_._3 > 0.bind)
+ .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" 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 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.pullRequest is true.bind, onlyPullRequest) &&
(IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in
@@ -164,7 +184,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 +199,8 @@
content,
false,
currentDate,
- currentDate)
+ currentDate,
+ isPullRequest)
// increment issue id
IssueId
@@ -250,39 +271,44 @@
val keywords = splitWords(query.toLowerCase)
// Search Issue
- val issues = Query(Issues).filter { t =>
- keywords.map { keyword =>
- (t.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) ||
- (t.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^'))
- } .reduceLeft(_ && _)
- }.map { t => (t, 0, t.content.?) }
+ 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 = Query(IssueComments).innerJoin(Issues).on { case (t1, t2) =>
- t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
- }.filter { case (t1, t2) =>
- keywords.map { query =>
- t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^')
- }.reduceLeft(_ && _)
- }.map { case (t1, t2) => (t2, t1.commentId, t1.content.?) }
+ 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)
+ }
- def getCommentCount(issue: Issue): Int = {
- Query(IssueComments)
- .filter { t =>
- t.byIssue(issue.userName, issue.repositoryName, issue.issueId) &&
- (t.action inSetBind Seq("comment", "close_comment", "reopen_comment"))
- }
- .map(_.issueId)
- .list.length
- }
-
- issues.union(comments).sortBy { case (issue, commentId, _) =>
+ issues.union(comments).sortBy { case (issue, commentId, _, _) =>
issue.issueId ~ commentId
- }.list.splitWith { case ((issue1, _, _), (issue2, _, _)) =>
+ }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) =>
issue1.issueId == issue2.issueId
- }.map { result =>
- val (issue, _, content) = result.head
- (issue, getCommentCount(issue) , content.getOrElse(""))
+ }.map { _.head match {
+ case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse(""))
+ }
}.toList
}
@@ -333,6 +359,13 @@
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..ab28675
--- /dev/null
+++ b/src/main/scala/service/PullRequestService.scala
@@ -0,0 +1,57 @@
+package service
+
+import scala.slick.driver.H2Driver.simple._
+import Database.threadLocalSession
+
+import model._
+
+trait PullRequestService { self: IssuesService =>
+ import PullRequestService._
+
+ def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = {
+ val issue = getIssue(owner, repository, issueId.toString)
+ if(issue.isDefined){
+ Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match {
+ case Some(pullreq) => Some((issue.get, pullreq))
+ case None => None
+ }
+ } else None
+ }
+
+ def getPullRequestCount(closed: Boolean, repository: Option[(String, 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 repository.get._1, repository.isDefined) &&
+ (t1.repositoryName is repository.get._2, repository.isDefined)
+ }
+ .groupBy { case (t1, t2) => t2.openedUserName }
+ .map { case (userName, t) => userName ~ t.length }
+ .list
+ .map { x => PullRequestCount(x._1, x._2) }
+ .sortBy(_.count).reverse
+
+ 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)
+
+}
\ No newline at end of file
diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala
index b8c7596..b03a0b5 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)
}
@@ -54,39 +62,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 +71,62 @@
*/
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)
+ new RepositoryInfo(
+ JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
+ repository,
+ 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)
+ ))
+ }
}
/**
@@ -189,17 +192,39 @@
}
}
+ // TODO It must be _.length instead of map (_.issueId) list).length.
+ // But it does not work on Slick 1.0.1 (worked on Slick 1.0.0).
+ // https://github.com/slick/slick/issues/170
+ private def getForkedCount(userName: String, repositoryName: String): Int =
+ Query(Repositories).filter { t =>
+ (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind)
+ }.list.length
+
+
+ def getForkedRepositoryTree(userName: String, repositoryName: String): RepositoryTreeNode = {
+ RepositoryTreeNode(userName, repositoryName,
+ Query(Repositories).filter { t =>
+ (t.parentUserName is userName.bind) && (t.parentRepositoryName is repositoryName.bind)
+ }.map { t =>
+ t.userName ~ t.repositoryName
+ }.list.map { case (userName, repositoryName) =>
+ getForkedRepositoryTree(userName, repositoryName)
+ }
+ )
+ }
+
}
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository,
- commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){
+ 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)
+ def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = {
+ this(repo.owner, repo.name, repo.url, model, 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/WikiService.scala b/src/main/scala/service/WikiService.scala
index a9be43d..49f30aa 100644
--- a/src/main/scala/service/WikiService.scala
+++ b/src/main/scala/service/WikiService.scala
@@ -4,10 +4,7 @@
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.treewalk.CanonicalTreeParser
-import java.util.concurrent.ConcurrentHashMap
+import util.{Directory, JGitUtil, LockUtil}
object WikiService {
@@ -31,40 +28,13 @@
*/
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(loginAccount: model.Account, owner: String, repository: String): Unit = {
- lock(owner, repository){
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
val dir = Directory.getWikiRepositoryDir(owner, repository)
if(!dir.exists){
try {
@@ -126,7 +96,7 @@
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Unit = {
- lock(owner, repository){
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy
val workDir = Directory.getWikiWorkDir(owner, repository)
cloneOrPullWorkingCopy(workDir, owner, repository)
@@ -162,8 +132,9 @@
/**
* Delete the wiki page.
*/
- def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = {
- lock(owner, repository){
+ def deleteWikiPage(owner: String, repository: String, pageName: String,
+ committer: String, mailAddress: String, message: String): Unit = {
+ LockUtil.lock(s"${owner}/${repository}/wiki"){
// clone working copy
val workDir = Directory.getWikiWorkDir(owner, repository)
cloneOrPullWorkingCopy(workDir, owner, repository)
@@ -175,34 +146,12 @@
git.rm.addFilepattern(pageName + ".md").call
// commit and push
- // TODO committer's mail address
- git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call
+ git.commit.setAuthor(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){
val git =
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala
index 601af9b..51f8c2c 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/AutoUpdateListener.scala
@@ -49,6 +49,7 @@
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
+ Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection): Unit = {
diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala
index f42c68d..b647d53 100644
--- a/src/main/scala/util/JGitUtil.scala
+++ b/src/main/scala/util/JGitUtil.scala
@@ -15,6 +15,7 @@
import org.eclipse.jgit.errors.MissingObjectException
import java.util.Date
import org.eclipse.jgit.api.errors.NoHeadException
+import service.RepositoryService
/**
* Provides complex JGit operations.
@@ -132,15 +133,18 @@
}
/**
- * 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
}
@@ -152,12 +156,7 @@
withGit(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",
@@ -294,6 +293,32 @@
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
+ }
+
+ val revWalk = new RevWalk(git.getRepository)
+ revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin)))
+
+ val commits = getCommitLog(revWalk.iterator, Nil)
+ revWalk.release
+
+ commits.reverse
+ }
+
/**
* Returns the commit list between two revisions.
@@ -303,30 +328,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.
@@ -348,51 +352,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
}
/**
@@ -426,7 +390,7 @@
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)))
@@ -438,26 +402,8 @@
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")))
- }
- }.toList
+ getDiffs(git, oldCommit.getName, id, fetchContent)
+
} else {
// initial commit
val walk = new TreeWalk(git.getRepository)
@@ -476,6 +422,27 @@
}
}
+ 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(new String(_, "UTF-8")),
+ JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
+ }
+ }.toList
+ }
+
+
/**
* Returns the list of branch names of the specified commit.
*/
@@ -524,6 +491,15 @@
}
}
+ def cloneRepository(from: java.io.File, to: java.io.File): Unit = {
+ val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call
+ try {
+ setReceivePack(git.getRepository)
+ } finally {
+ git.getRepository.close
+ }
+ }
+
def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null
private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = {
@@ -532,4 +508,14 @@
config.save
}
+ def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo,
+ revstr: String = ""): Option[(ObjectId, String)] = {
+ Seq(
+ if(revstr.isEmpty) repository.repository.defaultBranch else revstr,
+ repository.branchList.head
+ ).map { rev =>
+ (git.getRepository.resolve(rev), rev)
+ }.find(_._1 != null)
+ }
+
}
diff --git a/src/main/scala/util/LockUtil.scala b/src/main/scala/util/LockUtil.scala
new file mode 100644
index 0000000..3b6c796
--- /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}
+
+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 = {
+ val lock = getLockObject(key)
+ try {
+ lock.lock()
+ f
+ } finally {
+ lock.unlock()
+ }
+ }
+
+}
diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala
index eb47085..c5ae04e 100644
--- a/src/main/scala/view/AvatarImageProvider.scala
+++ b/src/main/scala/view/AvatarImageProvider.scala
@@ -13,18 +13,18 @@
protected def getAvatarImageHtml(userName: String, size: Int,
mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = {
- val src = if(getSystemSettings().gravatar){
- getAccountByUserName(userName).collect { case account if(account.image.isEmpty && !account.isGroupAccount) =>
+ val src = getAccountByUserName(userName).map { account =>
+ if(account.image.isEmpty && getSystemSettings().gravatar){
s"""http://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress)}?s=${size}"""
- } getOrElse {
- if(mailAddress.nonEmpty){
- s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
- } else {
- s"""${context.path}/${userName}/_avatar"""
- }
+ } else {
+ s"""${context.path}/${userName}/_avatar"""
}
- } else {
- s"""${context.path}/${userName}/_avatar"""
+ } getOrElse {
+ if(mailAddress.nonEmpty && getSystemSettings().gravatar){
+ s"""http://www.gravatar.com/avatar/${StringUtil.md5(mailAddress)}?s=${size}"""
+ } else {
+ s"""${context.path}/${userName}/_avatar"""
+ }
}
if(tooltip){
diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala
index 2d9c0e0..6613a5f 100644
--- a/src/main/scala/view/helpers.scala
+++ b/src/main/scala/view/helpers.scala
@@ -51,9 +51,17 @@
def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html =
Html(convertRefsLinks(value, repository))
+ def cut(value: String, length: Int): String =
+ if(value.length > length){
+ value.substring(0, length) + "..."
+ } else {
+ value
+ }
+
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""")
diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html
index de47a3d..97045d9 100644
--- a/src/main/twirl/account/repositories.scala.html
+++ b/src/main/twirl/account/repositories.scala.html
@@ -15,6 +15,9 @@
}
+ @if(repository.repository.originUserName.isDefined){
+
+ }
@if(repository.repository.description.isDefined){
@repository.repository.description
}
diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html
index b1839ab..9ebdede 100644
--- a/src/main/twirl/dashboard/issues.scala.html
+++ b/src/main/twirl/dashboard/issues.scala.html
@@ -2,7 +2,7 @@
allCount: Int,
assignedCount: Int,
createdByCount: Int,
- repositories: List[service.RepositoryService.RepositoryInfo],
+ repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context)
@import context._
@@ -13,19 +13,19 @@
- @repositories.map { repository =>
- -
-
- 0
- @repository.owner/@repository.name
+ @repositories.map { case (owner, name, count) =>
+
-
+
+ @count
+ @owner/@name
}
diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html
index 1ffc620..c2e42a5 100644
--- a/src/main/twirl/dashboard/tab.scala.html
+++ b/src/main/twirl/dashboard/tab.scala.html
@@ -1,8 +1,8 @@
@(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..c29a1f5 100644
--- a/src/main/twirl/header.scala.html
+++ b/src/main/twirl/header.scala.html
@@ -1,11 +1,24 @@
@(active: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
+
@repository.owner /
@repository.name
@if(repository.repository.isPrivate){
}
+ @defining(repository.repository){ x =>
+ @if(repository.repository.originRepositoryName.isDefined){
+
+ }
+ }
+
diff --git a/src/main/twirl/helper/dropdown.scala.html b/src/main/twirl/helper/dropdown.scala.html
index 6c86e96..cccd070 100644
--- a/src/main/twirl/helper/dropdown.scala.html
+++ b/src/main/twirl/helper/dropdown.scala.html
@@ -1,9 +1,12 @@
-@(buttonValue: String = "")(body: Html)
+@(buttonValue: String = "", prefix: String = "")(body: Html)
}
- - Files
+ - Files
- Commits
- - Tags@if(repository.tags.length > 0){ @repository.tags.length}
+ - Tags@if(repository.tags.length > 0){ @repository.tags.length}
-