-
+ @tables.map { table =>
+
- @table.name
+
-
+ @table.columns.map { column =>
+
- @column.name + @if(column.primaryKey){ (PK) } + + } +
+ }
+
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 507d345c7..fc43dd889 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,7 +1,6 @@
-# Guideline for Issues
+# The guidelines for contributing
-- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
-- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
-- We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
-- Write an issue in English. At least, write subject in English.
-- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
+- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues and pull requests whether there is a same request in the past.
+- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. If you don't wanna waste your time to make a pull request, ask us about your idea at [gitter room](https://gitter.im/gitbucket/gitbucket) before staring your work.
+- You can edit the GitBucket documentation on Wiki if you have a GitHub account. When you find any mistakes or lacks in the documentation, please update it directly.
+- All your contributions are handled as [Apache Software License, Version 2.0](https://github.com/gitbucket/gitbucket/blob/master/LICENSE). When you create a pull request or update the documentation, we assume you agreed this clause.
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 464249075..13e1c46b5 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,4 +1,4 @@
-### Before submitting an issue to Gitbucket I have first:
+### Before submitting an issue to GitBucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] searched for similar already existing issue
@@ -9,11 +9,10 @@
## Issue
**Impacted version**: xxxx
-**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
+**Deployment mode**: *explain here how you use GitBucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
**Problem description**:
- *be as explicit has you can*
- *describe the problem and its symptoms*
- *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)*
-- *do your best to use a correct english (re-read yourself)*
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 981e7732a..cbc726ccb 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,4 +1,4 @@
-### Before submitting a pull-request to Gitbucket I have first:
+### Before submitting a pull-request to GitBucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] rebased my branch over master
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
new file mode 100644
index 000000000..594943de2
--- /dev/null
+++ b/.github/SUPPORT.md
@@ -0,0 +1,5 @@
+# The support guidelines
+
+- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
+- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
+- Write issues in English if it's possible. It enables many of contributors to help you.
diff --git a/.gitignore b/.gitignore
index 8f141005b..d2d9e2240 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ project/plugins/project/
.classpath
.project
.cache
+.cache-main
+.cache-tests
.settings
# IntelliJ specific
diff --git a/.travis.yml b/.travis.yml
index 8eb0548f4..dc863a823 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,13 @@
language: scala
sudo: true
+jdk:
+ - oraclejdk8
script:
- sbt test
-jdk:
- - oraclejdk8
before_script:
- - sudo apt-get install libaio1
- sudo /etc/init.d/mysql stop
- sudo /etc/init.d/postgresql stop
+ - sudo chmod +x /usr/local/bin/sbt
cache:
directories:
- $HOME/.ivy2/cache
@@ -16,23 +16,3 @@ cache:
- $HOME/.coursier
- $HOME/.embedmysql
- $HOME/.embedpostgresql
-matrix:
- include:
- - dist: trusty
- group: edge
- sudo: required
- jdk: oraclejdk9
- script:
- # https://github.com/sbt/sbt/pull/2951
- - git clone https://github.com/retronym/java9-rt-export
- - cd java9-rt-export/
- - git checkout 1019a2873d057dd7214f4135e84283695728395d
- - jdk_switcher use oraclejdk8
- - sbt package
- - jdk_switcher use oraclejdk9
- - mkdir -p $HOME/.sbt/0.13/java9-rt-ext; java -jar target/java9-rt-export-*.jar $HOME/.sbt/0.13/java9-rt-ext/rt.jar
- - jar tf $HOME/.sbt/0.13/java9-rt-ext/rt.jar | grep java/lang/Object
- - cd ..
- - echo "sbt.version=0.13.14-RC1" > project/build.properties
- - wget https://raw.githubusercontent.com/paulp/sbt-extras/9ade5fa54914ca8aded44105bf4b9a60966f3ccd/sbt && chmod +x ./sbt
- - ./sbt -Dscala.ext.dirs=$HOME/.sbt/0.13/java9-rt-ext test
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..d9110fb01
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,457 @@
+# Changelog
+All changes to the project will be documented in this file.
+
+## 4.20.0 - 23 Dec 2017
+
+- Squash and rebase merge strategy for pull requests
+- Quick pull request creation
+- Download patch from the diff view
+- Fork and create repository are proceeded asynchronously
+- Create new repository by copying existing git repository
+- Hide overflowed repository names in the sidebar
+- Support CreateEvent web hook
+- Display conflicting files if pull request can't be merged
+
+## 4.19.3 - 7 Dec 2017
+
+- Fix file uploading bug
+- Fix reply comment form behavior in the diff view
+
+## 4.19.2 - 3 Dec 2017
+
+- Fix routing bug in `CompositeScalatraFilter`
+- Resolve id attribute collision in the web hook editing form
+
+## 4.19.1 - 2 Dec 2017
+
+- Update gitbucket-notifications-plugin because it had a version compatibility issue
+
+## 4.19.0 - 2 Dec 2017
+- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available
+- Upgrade to Scalatra 2.6
+- Improve layout of the system settings page
+- New extension point (`sshCommandProvider`)
+- Dropped [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin) from bundled plugins temporary because we couldn't complete update for Scalatra 2.6 before this release.
+
+## 4.18.0 - 14 Oct 2017
+- Form to reply to review comment
+- Display fullname in username suggestion
+- Commit hook plugins are applied to online editing
+- Improve gitbucket-ci-plugin
+
+## 4.17.0 - 30 Sep 2017
+- [gitbucket-ci-plugin](https://github.com/takezoe/gitbucket-ci-plugin) is available
+- Transferring to URL with commit ID
+- Drop uploadable file type limitation
+- Improve Mailer API
+- Web API and webhook enhancement
+
+## 4.16.0 - 2 Sep 2017
+- Support AdminLTE color skin
+- Improve unexpected error handling
+- Show commit status on the commits list
+
+## 4.15.0 - 5 Aug 2017
+- Bundle GitBucket organization plugins
+- Notifications plugin
+- Plugin hot deployment
+- Update Slick to 3.2.1 from 3.2.0
+- Support ed25519 keys for SSH
+- Markdown preview in comment editing forms
+
+## 4.14.1 - 4 Jul 2017
+- Bug fix: Possibility of error in forking repository
+
+## 4.14 - 1 Jul 2017
+- Support priority in issues and pull requests
+- Show icons when the sidebar is collapsed
+- Support gollum events in web hook
+- Support account (user / group) level web hook
+- Add `--max_file_size` option
+- Configuration by system property or environment variable
+
+## 4.13 - 29 May 2017
+- Uploading files into the repository
+- HTML is available in Markdown
+- Added filter box to dropdown menus
+
+## 4.12 - 30 Apr 2017
+- [Gist plug-in](https://github.com/gitbucket/gitbucket-gist-plugin) provides JavaScript to embed snippet
+- Dropdown menu filter in the branch comparing page
+- Caution for the embedded H2 database
+
+## 4.11 - 1 Apr 2017
+- Deploy keys support
+- Auto generate avatar images
+- Collaborators of the private forked repository are copied from the original repository
+- Cache avatar images in the browser
+- New extension point to receive events about repository
+
+## 4.10 - 25 Feb 2017
+- Update to Scala 2.12, Scalatra 2.5 and Slick 3.2
+- Display file size in the file viewer
+
+## 4.9 - 29 Jan 2017
+- GitLFS support
+- Template for issues and pull requests
+- Manual label color editing
+- Account description
+- `--tmp-dir` option for standalone mode
+- More APIs for issues
+ - [List issues for a repository](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
+ - [Create an issue](https://developer.github.com/v3/issues/#create-an-issue)
+
+## 4.8 - 23 Dec 2016
+- Search for repository names from the global header
+- Filter repositories on the sidebar of the dashboard
+- Search issues and wiki
+- Keep pull request comments after new commits are pushed
+- New web API to get a single issue
+- Performance improvement for the repository viewer
+
+## 4.7.1 - 28 Nov 2016
+- Bug fix: group repositories are not shown in the your repositories list on the sidebar
+- Small performance improvement of the dashboard
+
+## 4.7 - 26 Nov 2016
+- New permission system
+- Dropdown filter for issue labels, milestones and assignees
+- Keep sidebar folding status
+- Link from milestone label to the issue list
+
+## 4.6 - 29 Oct 2016
+- Add disable option for forking
+- Add History button to wiki page
+- Git repository URL redirection for GitHub compatibility
+- Get-Content API improvement
+- Indicate who is group master in Members tab in group view
+
+## 4.5 - 29 Sep 2016
+- Attach files by dropping into textarea
+- Issues / Pull requests switcher in dashboard
+- HikariCP could be configured in `GITBUCKET_HOME/database.conf`
+- Improve Cookie security
+- Display commit count on the history button
+- Improve mobile view
+
+## 4.4 - 28 Aug 2016
+- Import a SQL dump file to the database
+- `go get` support in private repositories
+- Sort milestones by due date
+- apache-sshd has been updated to 1.2.0
+
+## 4.3 - 30 Jul 2016
+- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
+- User name suggestion
+- Add new web APIs and basic authentication support for API access
+- Root Endpoint
+ - [List endpoints](https://developer.github.com/v3/#root-endpoint)
+ - [List Branches](https://developer.github.com/v3/repos/branches/#list-branches)
+ - [Get contents](https://developer.github.com/v3/repos/contents/#get-contents)
+ - [Get a Reference](https://developer.github.com/v3/git/refs/#get-a-reference)
+ - [List Collaborators](https://developer.github.com/v3/repos/collaborators/#list-collaborators)
+ - [List user repositories](https://developer.github.com/v3/repos/#list-user-repositories)
+ - [Get a group](https://developer.github.com/v3/orgs/#get-an-organization)
+ - [List group repositories](https://developer.github.com/v3/repos/#list-organization-repositories)
+- Add new extension points
+ - `assetsMapping` : Supplies resources in plugin classpath as web assets
+ - `suggestionProvider` : Provides suggestion in the Markdown editing textarea
+ - `textDecorator` : Decorate text nodes in HTML which is converted from Markdown
+
+## 4.2.1 - 3 Jul 2016
+- Fix migration bug
+
+This is hotfix for a critical bug in migration. If you are new installation, use 4.2.0. But if you have an exisiting installation and it had been updated to 4.0 from 3.x, you must update to 4.2.1.
+
+## 4.2 - 2 Jul 2016
+- New UI based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE)
+- git gc
+- Issues and Wiki have been possible to be disabled
+- SMTP configuration test mail
+
+## 4.1 - 4 Jun 2016
+- Generic ssh user
+- Improve branch protection UI
+- Default value of pull request title
+
+## 4.0 - 30 Apr 2016
+- MySQL and PostgreSQL support
+- Data export and import
+- Migration system has been switched to [solidbase](https://github.com/gitbucket/solidbase)
+
+**Note:** You can upgrade to GitBucket 4.0 from 3.14. If your GitBucket is 3.13 or before, you have to upgrade 3.14 at first.
+
+## 3.14 - 30 Apr 2016
+- File attachment and search for wiki pages
+- New extension points to add menus
+- Content-Type of webhooks has been choosable
+
+## 3.13 - 1 Apr 2016
+- Refresh user interface for wide screen
+- Add `pull_request` key in list issues API for pull requests
+- Add `X-Hub-Signature` security to webhooks
+- Provide SHA-256 checksum for `gitbucket.war`
+
+## 3.12 - 27 Feb 2016
+- New GitHub UI
+- Improve mobile view
+- Improve printing style
+- Individual URL for pull request tabs
+- SSH host configuration is separated from HTTP base URL
+
+## 3.11 - 30 Jan 2016
+- Upgrade Scalatra to 2.4
+- Sidebar and Footer for Wiki
+- Branch protection and receive hook extension point for plug-in
+- Limit recent updated repositories list
+- Issue actions look-alike GitHub
+- Web API for labels
+- Requires Java 8
+
+## 3.10 - 30 Dec 2015
+- Move to Bootstrap3
+- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
+- Update xsbt-web-plugin
+- Update H2 database
+
+## 3.9 - 5 Dec 2015
+- GFM inline breaks support in Markdown
+- WebHook on create review comment is available
+- WebHook event trigger is selectable
+
+## 3.8 - 31 Oct 2015
+- Moved to GitHub organization
+- Omit diff view for large differences
+- Repository creation API
+- Render url as link in repository description
+- Expand attachable file types
+
+## 3.7 - 3 Oct 2015
+- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
+- Clone in desktop button
+- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
+
+## 3.6 - 30 Aug 2015
+- User interface Improvements: Especially, commit list, issues and pull request have been updated largely.
+- Installed plugins list has been available at the system administration console.
+- Pages and repository list in the sidebar have been limited and more pages and repositories link is available.
+- More reference link notation in Markdown has been supported.
+
+## 3.5 - 1 Aug 2015
+- Octicons has been applied
+- Global header has been enhanced. Now it's further similar to GitHub.
+- Default compare / pull request target has been changed to the parent repository
+- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
+
+## 3.4 - 27 Jun 2015
+- Declarative style plug-in definition
+- New extension point to add markup render
+- go-import support
+
+## 3.3 - 31 May 2015
+- Rich graphical diff for images
+- File finder is available in the repository viewer
+- Blame is displayed at the source viewer
+- Remain user data and repositories even if user is disabled
+- Mobile view improvement
+
+## 3.2 - 3 May 2015
+- Directory history button
+- Compare / pull request button
+- Limit of activity log
+
+## 3.1.1 - 4 Apr 2015
+- Rolled back H2 version to avoid version compatibility issue
+- Plug-ins became possible to access ServletContext
+
+## 3.1 - 28 Mar 2015
+- Web APIs for Jenkins github pull-request builder
+- Improved diff view
+- Bump Scalatra to 2.3.1, sbt to 0.13.8
+
+## 3.0 - 3 Mar 2015
+- New plug-in system is available
+- Connection pooling by c3p0
+- New branch UI
+- Compare between specified commit ids
+
+## 2.8 - 1 Feb 2015
+- New logo and icons
+- New system setting options to control visibility
+- Comment on side-by-side diff
+- Information message on sign-in page
+- Fork repository by group account
+
+## 2.7 - 29 Dec 2014
+- Comment for commit and diff
+- Fix security issue in markdown rendering
+- Some bug fix and improvements
+
+## 2.6 - 24 Nov 2014
+- Search box at issues and pull requests
+- Information from administrator
+- Pull request UI has been updated
+- Move to TravisCI from Buildhive
+- Some bug fix and improvements
+
+## 2.5 - 4 Nov 2014
+- New Dashboard
+- Change datetime format
+- Create branch from Web UI
+- Task list in Markdown
+- Some bug fix and improvements
+
+## 2.4.1 - 6 Oct 2014
+- Bug fix
+
+## 2.4 - 6 Oct 2014
+- New UI is applied to Issues and Pull requests
+- Side-by-side diff is available
+- Fix relative path problem in Markdown links and images
+- Plugin System is disabled in default
+- Some bug fix and improvements
+
+## 2.3 - 1 Sep 2014
+- Scala based plugin system
+- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
+- Some bug fix and improvements
+
+## 2.2.1 - 5 Aug 2014
+- Bug fix
+
+## 2.2 - 4 Aug 2014
+- Plug-in system is available
+- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
+- tar.gz export for repository contents
+- LDAP authentication improvement (mail address became optional)
+- Show news feed of a private repository to members
+- Some bug fix and improvements
+
+## 2.1 - 6 Jul 2014
+- Upgrade to Slick 2.0 from 1.9
+- Base part of the plug-in system is merged
+- Many bug fix and improvements
+
+## 2.0 - 31 May 2014
+- Modern Github UI
+- Preview in AceEditor
+- Select lines by clicking line number in blob view
+
+## 1.13 - 29 Apr 2014
+- Direct file editing in the repository viewer using AceEditor
+- File attachment for issues
+- Atom feed of user activity
+- Fix some bugs
+
+## 1.12 - 29 Mar 2014
+- SSH repository access is available
+- Allow users can create and management their groups
+- Git submodule support
+- Close issues via commit messages
+- Show repository description below the name on repository page
+- Fix presentation of the source viewer
+- Upgrade to sbt 0.13
+- Fix some bugs
+
+## 1.11.1 - 06 Mar 2014
+- Bug fix
+
+## 1.11 - 01 Mar 2014
+- Base URL for redirection, notification and repository URL box is configurable
+- Remove ```--https``` option because it's possible to substitute in the base url
+- Headline anchor is available for Markdown contents such as Wiki page
+- Improve H2 connectivity
+- Label is available for pull requests not only issues
+- Delete branch button is added
+- Repository icons are updated
+- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
+- Display reference to issue from others in comment list
+- Fix some bugs
+
+## 1.10 - 01 Feb 2014
+- Rename repository
+- Transfer repository owner
+- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
+- Add LDAP display name attribute
+- Response performance improvement
+- Fix some bugs
+
+## 1.9 - 28 Dec 2013
+- Display GITBUCKET_HOME on the system settings page
+- Fix some bugs
+
+## 1.8 - 30 Nov 2013
+- Add user and group deletion
+- Improve pull request performance
+- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
+- LDAP StartTLS support
+- Enable hard wrapping in Markdown
+- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
+- Fix some bugs
+
+## 1.7 - 26 Oct 2013
+- Support working on Java6 in embedded Jetty mode
+- Add `--host` option to bind specified host name in embedded Jetty mode
+- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
+- Add full name as user property
+- Change link color for absent Wiki pages
+- Add ZIP download button to the repository viewer tab
+- Improve ZIP exporting performance
+- Expand issue and comment textarea for long text automatically
+- Add conflict detection in Wiki
+- Add reverting wiki page from history
+- Match committer to user name by email address
+- Mail notification sender is customizable
+- Add link to changeset in refs comment for issues
+- Fix some bugs
+
+## 1.6 - 1 Oct 2013
+- Web hook
+- Performance improvement for pull request
+- Executable war file
+- Specify suitable Content-Type for downloaded files in the repository viewer
+- Fix some bugs
+
+## 1.5 - 4 Sep 2013
+- Fork and pull request
+- LDAP authentication
+- Mail notification
+- Add an option to turn off the gravatar support
+- Add the branch tab in the repository viewer
+- Encoding auto detection for the file content in the repository viewer
+- Add favicon, header logo and icons for the timeline
+- Specify data directory via environment variable GITBUCKET_HOME
+- Fix some bugs
+
+## 1.4 - 31 Jul 2013
+- Group management
+- Repository search for code and issues
+- Display user related issues on the dashboard
+- Display participants avatar of issues on the issue page
+- Performance improvement for repository viewer
+- Alert by milestone due date
+- H2 database administration console
+- Fix some bugs
+
+## 1.3 - 18 Jul 2013
+- Batch updating for issues
+- Display assigned user on issue list
+- User icon and Gravatar support
+- Convert @xxxx to link to the account page
+- Add copy to clipboard button for git clone URL
+- Allow multi-byte characters as wiki page name
+- Allow to create the empty repository
+- Fix some bugs
+
+## 1.2 - 09 Jul 2013
+- Add activity timeline
+- Bugfix for Git 1.8.1.5 or later
+- Allow multi-byte characters as label
+- Fix some bugs
+
+## 1.1 - 05 Jul 2013
+- Fix some bugs
+- Upgrade to JGit 3.0
+
+## 1.0 - 04 Jul 2013
+- This is a first public release
diff --git a/README.md b/README.md
index 4145be584..53472b43c 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,12 @@ You can specify following options:
- `--host=[HOSTNAME]`
- `--gitbucket.home=[DATA_DIR]`
- `--temp_dir=[TEMP_DIR]`
+- `--max_file_size=[MAX_FILE_SIZE]`
`TEMP_DIR` is used as the [temporary directory for the jetty application context](https://www.eclipse.org/jetty/documentation/9.3.x/ref-temporary-directories.html). This is the directory into which the `gitbucket.war` file is unpacked, the source files are compiled, etc. If given this parameter **must** match the path of an existing directory or the application will quit reporting an error; if not given the path used will be a `tmp` directory inside the gitbucket home.
+`MAX_FILE_SIZE` is the max file size for upload files.
+
You can also deploy `gitbucket.war` to a servlet container which supports Servlet 3.0 (like Jetty, Tomcat, JBoss, etc)
For more information about installation on Mac or Windows Server (with IIS), or configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
@@ -54,392 +57,29 @@ GitBucket has a plug-in system that allows extra functionality. Officially the f
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
+- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin)
-You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/).
+You can find more plugins made by the community at [GitBucket community plugins](https://gitbucket-plugins.github.io/).
Support
--------
- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
-- We can also provide support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
-- Write an issue in English. At least, write subject in English.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
-Release Notes
+What's New in 4.20.x
-------------
-### 4.11 - 1 Apr 2017
-- Deploy keys support
-- Auto generate avatar images
-- Collaborators of the private forked repository are copied from the original repository
-- Cache avatar images in the browser
-- New extension point to receive events about repository
-### 4.10 - 25 Feb 2017
-- Update to Scala 2.12, Scalatra 2.5 and Slick 3.2
-- Display file size in the file viewer
+### 4.20.0 - 23 Dec 2017
-### 4.9 - 29 Jan 2017
-- GitLFS support
-- Template for issues and pull requests
-- Manual label color editing
-- Account description
-- `--tmp-dir` option for standalone mode
-- More APIs for issues
- - [List issues for a repository](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
- - [Create an issue](https://developer.github.com/v3/issues/#create-an-issue)
+- Squash and rebase merge strategy for pull requests
+- Quick pull request creation
+- Download patch from the diff view
+- Fork and create repository are proceeded asynchronously
+- Create new repository by copying existing git repository
+- Hide overflowed repository names in the sidebar
+- Support CreateEvent web hook
+- Display conflicting files if pull request can't be merged
-### 4.8 - 23 Dec 2016
-- Search for repository names from the global header
-- Filter repositories on the sidebar of the dashboard
-- Search issues and wiki
-- Keep pull request comments after new commits are pushed
-- New web API to get a single issue
-- Performance improvement for the repository viewer
-
-### 4.7.1 - 28 Nov 2016
-- Bug fix: group repositories are not shown in the your repositories list on the sidebar
-- Small performance improvement of the dashboard
-
-### 4.7 - 26 Nov 2016
-- New permission system
-- Dropdown filter for issue labels, milestones and assignees
-- Keep sidebar folding status
-- Link from milestone label to the issue list
-
-### 4.6 - 29 Oct 2016
-- Add disable option for forking
-- Add History button to wiki page
-- Git repository URL redirection for GitHub compatibility
-- Get-Content API improvement
-- Indicate who is group master in Members tab in group view
-
-### 4.5 - 29 Sep 2016
-- Attach files by dropping into textarea
-- Issues / Pull requests switcher in dashboard
-- HikariCP could be configured in `GITBUCKET_HOME/database.conf`
-- Improve Cookie security
-- Display commit count on the history button
-- Improve mobile view
-
-### 4.4 - 28 Aug 2016
-- Import a SQL dump file to the database
-- `go get` support in private repositories
-- Sort milestones by due date
-- apache-sshd has been updated to 1.2.0
-
-### 4.3 - 30 Jul 2016
-- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
-- User name suggestion
-- Add new web APIs and basic authentication support for API access
-- Root Endpoint
- - [List endpoints](https://developer.github.com/v3/#root-endpoint)
- - [List Branches](https://developer.github.com/v3/repos/branches/#list-branches)
- - [Get contents](https://developer.github.com/v3/repos/contents/#get-contents)
- - [Get a Reference](https://developer.github.com/v3/git/refs/#get-a-reference)
- - [List Collaborators](https://developer.github.com/v3/repos/collaborators/#list-collaborators)
- - [List user repositories](https://developer.github.com/v3/repos/#list-user-repositories)
- - [Get a group](https://developer.github.com/v3/orgs/#get-an-organization)
- - [List group repositories](https://developer.github.com/v3/repos/#list-organization-repositories)
-- Add new extension points
- - `assetsMapping` : Supplies resources in plugin classpath as web assets
- - `suggestionProvider` : Provides suggestion in the Markdown editing textarea
- - `textDecorator` : Decorate text nodes in HTML which is converted from Markdown
-
-### 4.2.1 - 3 Jul 2016
-- Fix migration bug
-
-This is hotfix for a critical bug in migration. If you are new installation, use 4.2.0. But if you have an exisiting installation and it had been updated to 4.0 from 3.x, you must update to 4.2.1.
-
-### 4.2 - 2 Jul 2016
-- New UI based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE)
-- git gc
-- Issues and Wiki have been possible to be disabled
-- SMTP configuration test mail
-
-### 4.1 - 4 Jun 2016
-- Generic ssh user
-- Improve branch protection UI
-- Default value of pull request title
-
-### 4.0 - 30 Apr 2016
-- MySQL and PostgreSQL support
-- Data export and import
-- Migration system has been switched to [solidbase](https://github.com/gitbucket/solidbase)
-
-**Note:** You can upgrade to GitBucket 4.0 from 3.14. If your GitBucket is 3.13 or before, you have to upgrade 3.14 at first.
-
-### 3.14 - 30 Apr 2016
-- File attachment and search for wiki pages
-- New extension points to add menus
-- Content-Type of webhooks has been choosable
-
-### 3.13 - 1 Apr 2016
-- Refresh user interface for wide screen
-- Add `pull_request` key in list issues API for pull requests
-- Add `X-Hub-Signature` security to webhooks
-- Provide SHA-256 checksum for `gitbucket.war`
-
-### 3.12 - 27 Feb 2016
-- New GitHub UI
-- Improve mobile view
-- Improve printing style
-- Individual URL for pull request tabs
-- SSH host configuration is separated from HTTP base URL
-
-### 3.11 - 30 Jan 2016
-- Upgrade Scalatra to 2.4
-- Sidebar and Footer for Wiki
-- Branch protection and receive hook extension point for plug-in
-- Limit recent updated repositories list
-- Issue actions look-alike GitHub
-- Web API for labels
-- Requires Java 8
-
-### 3.10 - 30 Dec 2015
-- Move to Bootstrap3
-- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
-- Update xsbt-web-plugin
-- Update H2 database
-
-### 3.9 - 5 Dec 2015
-- GFM inline breaks support in Markdown
-- WebHook on create review comment is available
-- WebHook event trigger is selectable
-
-### 3.8 - 31 Oct 2015
-- Moved to GitHub organization
-- Omit diff view for large differences
-- Repository creation API
-- Render url as link in repository description
-- Expand attachable file types
-
-### 3.7 - 3 Oct 2015
-- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
-- Clone in desktop button
-- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
-
-### 3.6 - 30 Aug 2015
-- User interface Improvements: Especially, commit list, issues and pull request have been updated largely.
-- Installed plugins list has been available at the system administration console.
-- Pages and repository list in the sidebar have been limited and more pages and repositories link is available.
-- More reference link notation in Markdown has been supported.
-
-### 3.5 - 1 Aug 2015
-- Octicons has been applied
-- Global header has been enhanced. Now it's further similar to GitHub.
-- Default compare / pull request target has been changed to the parent repository
-- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
-
-### 3.4 - 27 Jun 2015
-- Declarative style plug-in definition
-- New extension point to add markup render
-- go-import support
-
-### 3.3 - 31 May 2015
-- Rich graphical diff for images
-- File finder is available in the repository viewer
-- Blame is displayed at the source viewer
-- Remain user data and repositories even if user is disabled
-- Mobile view improvement
-
-### 3.2 - 3 May 2015
-- Directory history button
-- Compare / pull request button
-- Limit of activity log
-
-### 3.1.1 - 4 Apr 2015
-- Rolled back H2 version to avoid version compatibility issue
-- Plug-ins became possible to access ServletContext
-
-### 3.1 - 28 Mar 2015
-- Web APIs for Jenkins github pull-request builder
-- Improved diff view
-- Bump Scalatra to 2.3.1, sbt to 0.13.8
-
-### 3.0 - 3 Mar 2015
-- New plug-in system is available
-- Connection pooling by c3p0
-- New branch UI
-- Compare between specified commit ids
-
-### 2.8 - 1 Feb 2015
-- New logo and icons
-- New system setting options to control visibility
-- Comment on side-by-side diff
-- Information message on sign-in page
-- Fork repository by group account
-
-### 2.7 - 29 Dec 2014
-- Comment for commit and diff
-- Fix security issue in markdown rendering
-- Some bug fix and improvements
-
-### 2.6 - 24 Nov 2014
-- Search box at issues and pull requests
-- Information from administrator
-- Pull request UI has been updated
-- Move to TravisCI from Buildhive
-- Some bug fix and improvements
-
-### 2.5 - 4 Nov 2014
-- New Dashboard
-- Change datetime format
-- Create branch from Web UI
-- Task list in Markdown
-- Some bug fix and improvements
-
-### 2.4.1 - 6 Oct 2014
-- Bug fix
-
-### 2.4 - 6 Oct 2014
-- New UI is applied to Issues and Pull requests
-- Side-by-side diff is available
-- Fix relative path problem in Markdown links and images
-- Plugin System is disabled in default
-- Some bug fix and improvements
-
-### 2.3 - 1 Sep 2014
-- Scala based plugin system
-- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
-- Some bug fix and improvements
-
-### 2.2.1 - 5 Aug 2014
-- Bug fix
-
-### 2.2 - 4 Aug 2014
-- Plug-in system is available
-- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
-- tar.gz export for repository contents
-- LDAP authentication improvement (mail address became optional)
-- Show news feed of a private repository to members
-- Some bug fix and improvements
-
-### 2.1 - 6 Jul 2014
-- Upgrade to Slick 2.0 from 1.9
-- Base part of the plug-in system is merged
-- Many bug fix and improvements
-
-### 2.0 - 31 May 2014
-- Modern Github UI
-- Preview in AceEditor
-- Select lines by clicking line number in blob view
-
-### 1.13 - 29 Apr 2014
-- Direct file editing in the repository viewer using AceEditor
-- File attachment for issues
-- Atom feed of user activity
-- Fix some bugs
-
-### 1.12 - 29 Mar 2014
-- SSH repository access is available
-- Allow users can create and management their groups
-- Git submodule support
-- Close issues via commit messages
-- Show repository description below the name on repository page
-- Fix presentation of the source viewer
-- Upgrade to sbt 0.13
-- Fix some bugs
-
-### 1.11.1 - 06 Mar 2014
-- Bug fix
-
-### 1.11 - 01 Mar 2014
-- Base URL for redirection, notification and repository URL box is configurable
-- Remove ```--https``` option because it's possible to substitute in the base url
-- Headline anchor is available for Markdown contents such as Wiki page
-- Improve H2 connectivity
-- Label is available for pull requests not only issues
-- Delete branch button is added
-- Repository icons are updated
-- Select lines of source code by URL hash like `#L10` or `#L10-L15` in repository viewer
-- Display reference to issue from others in comment list
-- Fix some bugs
-
-### 1.10 - 01 Feb 2014
-- Rename repository
-- Transfer repository owner
-- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used.
-- Add LDAP display name attribute
-- Response performance improvement
-- Fix some bugs
-
-### 1.9 - 28 Dec 2013
-- Display GITBUCKET_HOME on the system settings page
-- Fix some bugs
-
-### 1.8 - 30 Nov 2013
-- Add user and group deletion
-- Improve pull request performance
-- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request)
-- LDAP StartTLS support
-- Enable hard wrapping in Markdown
-- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure).
-- Fix some bugs
-
-### 1.7 - 26 Oct 2013
-- Support working on Java6 in embedded Jetty mode
-- Add `--host` option to bind specified host name in embedded Jetty mode
-- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy
-- Add full name as user property
-- Change link color for absent Wiki pages
-- Add ZIP download button to the repository viewer tab
-- Improve ZIP exporting performance
-- Expand issue and comment textarea for long text automatically
-- Add conflict detection in Wiki
-- Add reverting wiki page from history
-- Match committer to user name by email address
-- Mail notification sender is customizable
-- Add link to changeset in refs comment for issues
-- Fix some bugs
-
-### 1.6 - 1 Oct 2013
-- Web hook
-- Performance improvement for pull request
-- Executable war file
-- Specify suitable Content-Type for downloaded files in the repository viewer
-- Fix some bugs
-
-### 1.5 - 4 Sep 2013
-- Fork and pull request
-- LDAP authentication
-- Mail notification
-- Add an option to turn off the gravatar support
-- Add the branch tab in the repository viewer
-- Encoding auto detection for the file content in the repository viewer
-- Add favicon, header logo and icons for the timeline
-- Specify data directory via environment variable GITBUCKET_HOME
-- Fix some bugs
-
-### 1.4 - 31 Jul 2013
-- Group management
-- Repository search for code and issues
-- Display user related issues on the dashboard
-- Display participants avatar of issues on the issue page
-- Performance improvement for repository viewer
-- Alert by milestone due date
-- H2 database administration console
-- Fix some bugs
-
-### 1.3 - 18 Jul 2013
-- Batch updating for issues
-- Display assigned user on issue list
-- User icon and Gravatar support
-- Convert @xxxx to link to the account page
-- Add copy to clipboard button for git clone URL
-- Allow multi-byte characters as wiki page name
-- Allow to create the empty repository
-- Fix some bugs
-
-### 1.2 - 09 Jul 2013
-- Add activity timeline
-- Bugfix for Git 1.8.1.5 or later
-- Allow multi-byte characters as label
-- Fix some bugs
-
-### 1.1 - 05 Jul 2013
-- Fix some bugs
-- Upgrade to JGit 3.0
-
-### 1.0 - 04 Jul 2013
-- This is a first public release
+See the [change log](CHANGELOG.md) for all of the updates.
diff --git a/build.sbt b/build.sbt
index a08aa2a6b..910b63405 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,16 +1,21 @@
+import com.typesafe.sbt.license.{LicenseInfo, DepModuleInfo}
+import com.typesafe.sbt.pgp.PgpKeys._
+
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
-val GitBucketVersion = "4.12.0-SNAPSHOT"
-val ScalatraVersion = "2.5.0"
-val JettyVersion = "9.3.9.v20160517"
+val GitBucketVersion = "4.20.0"
+val ScalatraVersion = "2.6.1"
+val JettyVersion = "9.4.7.v20170914"
-lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
+lazy val root = (project in file(".")).enablePlugins(SbtTwirl, ScalatraPlugin, JRebelPlugin).settings(
+
+)
sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
-scalaVersion := "2.12.2"
+scalaVersion := "2.12.4"
// dependency settings
resolvers ++= Seq(
@@ -21,46 +26,47 @@ resolvers ++= Seq(
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
- "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.7.0.201704051617-r",
- "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.7.0.201704051617-r",
+ "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.9.2.201712150930-r",
+ "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.9.2.201712150930-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
- "org.json4s" %% "json4s-jackson" % "3.5.0",
- "io.github.gitbucket" %% "scalatra-forms" % "1.1.0",
- "commons-io" % "commons-io" % "2.4",
- "io.github.gitbucket" % "solidbase" % "1.0.0",
- "io.github.gitbucket" % "markedj" % "1.0.10",
- "org.apache.commons" % "commons-compress" % "1.11",
- "org.apache.commons" % "commons-email" % "1.4",
- "org.apache.httpcomponents" % "httpclient" % "4.5.1",
- "org.apache.sshd" % "apache-sshd" % "1.2.0",
- "org.apache.tika" % "tika-core" % "1.13",
- "com.github.takezoe" %% "blocking-slick-32" % "0.0.8",
- "joda-time" % "joda-time" % "2.9.6",
+ "org.scalatra" %% "scalatra-forms" % ScalatraVersion,
+ "org.json4s" %% "json4s-jackson" % "3.5.3",
+ "commons-io" % "commons-io" % "2.6",
+ "io.github.gitbucket" % "solidbase" % "1.0.2",
+ "io.github.gitbucket" % "markedj" % "1.0.15",
+ "org.apache.commons" % "commons-compress" % "1.15",
+ "org.apache.commons" % "commons-email" % "1.5",
+ "org.apache.httpcomponents" % "httpclient" % "4.5.4",
+ "org.apache.sshd" % "apache-sshd" % "1.6.0" exclude("org.slf4j","slf4j-jdk14"),
+ "org.apache.tika" % "tika-core" % "1.17",
+ "com.github.takezoe" %% "blocking-slick-32" % "0.0.10",
"com.novell.ldap" % "jldap" % "2009-10-07",
- "com.h2database" % "h2" % "1.4.192",
- "mysql" % "mysql-connector-java" % "5.1.39",
- "org.postgresql" % "postgresql" % "9.4.1208",
- "ch.qos.logback" % "logback-classic" % "1.1.7",
- "com.zaxxer" % "HikariCP" % "2.4.6",
- "com.typesafe" % "config" % "1.3.0",
- "com.typesafe.akka" %% "akka-actor" % "2.4.12",
+ "com.h2database" % "h2" % "1.4.196",
+ "org.mariadb.jdbc" % "mariadb-java-client" % "2.2.1",
+ "org.postgresql" % "postgresql" % "42.1.4",
+ "ch.qos.logback" % "logback-classic" % "1.2.3",
+ "com.zaxxer" % "HikariCP" % "2.7.4",
+ "com.typesafe" % "config" % "1.3.2",
+ "com.typesafe.akka" %% "akka-actor" % "2.5.8",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
- "org.cache2k" % "cache2k-all" % "1.0.0.CR1",
- "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
+ "org.cache2k" % "cache2k-all" % "1.0.1.Final",
+ "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.1-akka-2.5.x" exclude("c3p0","c3p0"),
"net.coobird" % "thumbnailator" % "0.4.8",
+ "com.github.zafarkhaja" % "java-semver" % "0.9.0",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
- "org.mockito" % "mockito-core" % "2.7.16" % "test",
- "com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
- "ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
+ "org.mockito" % "mockito-core" % "2.13.0" % "test",
+ "com.wix" % "wix-embedded-mysql" % "3.0.0" % "test",
+ "ru.yandex.qatools.embed" % "postgresql-embedded" % "2.6" % "test",
+ "net.i2p.crypto" % "eddsa" % "0.2.0"
)
// Compiler settings
-scalacOptions := Seq("-deprecation", "-language:postfixOps")
+scalacOptions := Seq("-deprecation", "-language:postfixOps", "-opt:l:method")
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
@@ -85,17 +91,22 @@ assemblyMergeStrategy in assembly := {
}
// JRebel
-Seq(jrebelSettings: _*)
+//Seq(jrebelSettings: _*)
-jrebel.webLinks += (target in webappPrepare).value
-jrebel.enabled := System.getenv().get("JREBEL") != null
+//jrebel.webLinks += (target in webappPrepare).value
+//jrebel.enabled := System.getenv().get("JREBEL") != null
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
- Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
+ Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
+}
+
+// Exclude a war file from published artifacts
+signedArtifacts := {
+ signedArtifacts.value.filterNot { case (_, file) => file.getName.endsWith(".war") || file.getName.endsWith(".war.asc") }
}
// Create executable war file
-val executableConfig = config("executable").hide
-Keys.ivyConfigurations += executableConfig
+val ExecutableConfig = config("executable").hide
+Keys.ivyConfigurations += ExecutableConfig
libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
@@ -124,7 +135,7 @@ executableKey := {
IO delete temp
// include jetty classes
- val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
+ val jettyJars = Keys.update.value select configurationFilter(name = ExecutableConfig.name)
jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") ||
@@ -143,14 +154,25 @@ executableKey := {
IO copyFile (classDir / name, temp / name)
}
+ // include plugins
+ val pluginsDir = temp / "WEB-INF" / "classes" / "plugins"
+ IO createDirectory (pluginsDir)
+ IO copyFile(Keys.baseDirectory.value / "plugins.json", pluginsDir / "plugins.json")
+
+ val json = IO read(Keys.baseDirectory.value / "plugins.json")
+ PluginsJson.getUrls(json).foreach { url =>
+ log info s"Download: ${url}"
+ IO transfer(new java.net.URL(url).openStream, pluginsDir / url.substring(url.lastIndexOf("/") + 1))
+ }
+
// zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF")
- val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
+ val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file => IO.relativizeFile(temp, file) }
val manifest = new JarManifest
- manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
- manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
+ manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
+ manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName
- IO jar (contentMappings, outputFile, manifest)
+ IO jar (contentMappings.map { case (file, path) => (file, path.toString) } , outputFile, manifest)
// generate checksums
Seq(
@@ -170,7 +192,7 @@ executableKey := {
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
- else Some("releases" at nexus + "service/local/staging/deploy/maven2")
+ else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
publishMavenStyle := true
pomIncludeRepository := { _ => false }
@@ -219,3 +241,8 @@ pomExtra := (
)
+
+licenseOverrides := {
+ case DepModuleInfo("com.github.bkromhout", "java-diff-utils", _) =>
+ LicenseInfo(LicenseCategory.Apache, "Apache-2.0", "http://www.apache.org/licenses/LICENSE-2.0")
+}
diff --git a/doc/how_to_run.md b/doc/how_to_run.md
index 237dd68fc..9db078934 100644
--- a/doc/how_to_run.md
+++ b/doc/how_to_run.md
@@ -1,6 +1,12 @@
How to run from the source tree
========
+Install [sbt](http://www.scala-sbt.org/index.html) at first.
+
+```
+$ brew install sbt
+```
+
Run for Development
--------
diff --git a/doc/jrebel.md b/doc/jrebel.md
index 9ea1f68c2..4a597d929 100644
--- a/doc/jrebel.md
+++ b/doc/jrebel.md
@@ -1,35 +1,25 @@
JRebel integration (optional)
=============================
-[JRebel](http://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
-JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
+[JRebel](https://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
+JRebel is generally able to eliminate the need for the slow "app restart" per modification of codes. Alsp it's only used during development, and doesn't change your deployed app in any way.
-```
-> jetty:start
-```
-
-While JRebel is not open source, it does reload your code faster than the `~;copy-resources;aux-compile` way of doing things using `sbt`.
-
-It's only used during development, and doesn't change your deployed app in any way.
-
-JRebel used to be free for Scala developers, but that changed recently, and now there's a cost associated with usage for Scala. There are trial plans and free non-commercial licenses available if you just want to try it out.
+JRebel is not open source, but we can use it free for non-commercial use.
----
## 1. Get a JRebel license
-Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
+Sign up for a [myJRebel](https://my.jrebel.com/register). You will need to create an account.
## 2. Download JRebel
-Download the most recent ["nosetup" JRebel zip](http://zeroturnaround.com/software/jrebel/download/prev-releases/).
+Download the most recent ["nosetup" JRebel zip](https://zeroturnaround.com/software/jrebel/download/prev-releases/).
Next, unzip the downloaded file.
## 3. Activate
-Follow the [instructions on the JRebel website](http://zeroturnaround.com/software/jrebel/download/prev-releases/) to activate your downloaded JRebel.
-
-You can use the default settings for all the configurations.
+Follow `readme.txt` in the extracted directory to activate your downloaded JRebel.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
@@ -41,13 +31,13 @@ You only need to tell jvm where to find the jrebel jar.
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash
-export JREBEL=/path/to/jrebel/jrebel.jar
+export JREBEL=/path/to/jrebel/legacy/jrebel.jar
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
-export JREBEL=~/jrebel/jrebel.jar
+export JREBEL=~/jrebel/legacy/jrebel.jar
```
Now reload your shell:
@@ -73,39 +63,26 @@ $ ./sbt
You will start the servlet container slightly differently now that you're using sbt.
```
-> jetty:start
+> jetty:quickstart
:
-[info] starting server ...
-[success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
-2016-01-03 21:47:57 JRebel:
-2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
-2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
-2016-01-03 21:47:57 JRebel:
-2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
-2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
-2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
-2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: #############################################################
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: JRebel Legacy Agent 6.2.5 (201509291538)
-2016-01-03 21:48:00 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: Over the last 30 days JRebel prevented
-2016-01-03 21:48:00 JRebel: at least 182 redeploys/restarts saving you about 7.4 hours.
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: Over the last 324 days JRebel prevented
-2016-01-03 21:48:00 JRebel: at least 1538 redeploys/restarts saving you about 62.4 hours.
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: Licensed to nazo king (using myJRebel).
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel:
-2016-01-03 21:48:00 JRebel: #############################################################
-2016-01-03 21:48:00 JRebel:
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel: #############################################################
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel: Legacy Agent 7.0.15 (201709080836)
+2017-09-21 15:46:35 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel: Over the last 2 days JRebel prevented
+2017-09-21 15:46:35 JRebel: at least 8 redeploys/restarts saving you about 0.3 hours.
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel: Licensed to Naoki Takezoe (using myJRebel).
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel:
+2017-09-21 15:46:35 JRebel: #############################################################
+2017-09-21 15:46:35 JRebel:
:
-> ~ copy-resources
-[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
+> ~compile
+[success] Total time: 2 s, completed 2017/09/21 15:50:06
1. Waiting for source changes... (press enter to interrupt)
```
@@ -114,12 +91,11 @@ For example, you can change the title on `src/main/twirl/gitbucket/core/main.sca
```html
:
-
-
GitBucket
- @defining(AutoUpdate.getCurrentVersion){ version =>
- @version.majorVersion.@version.minorVersion
- }
+
+
+ GitBucket
change code !!!!!!!!!!!!!!!!
+ @gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion
:
```
@@ -128,21 +104,17 @@ If JRebel is doing is correctly installed you will see a notice for you:
```
1. Waiting for source changes... (press enter to interrupt)
-2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
-[info] Wrote rebel.xml to /git/gitbucket/target/scala-2.11/resource_managed/main/rebel.xml
-[info] Compiling 1 Scala source to /git/gitbucket/target/scala-2.11/classes...
-[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
-2. Waiting for source changes... (press enter to interrupt)
+[info] Compiling 1 Scala source to /Users/naoki.takezoe/gitbucket/target/scala-2.12/classes...
+[success] Total time: 1 s, completed 2017/09/21 15:55:40
```
And you reload browser, JRebel give notice of that it has reloaded classes:
```
-[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
-2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
+2017-09-21 15:55:40 JRebel: Reloading class 'gitbucket.core.html.main$'.
```
## 6. Limitations
-JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routes patterns, there is nothing JRebel can do, you will have to run `jetty:start`.
+JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routing patterns, there is nothing JRebel can do, you will have to restart by `jetty:quickstart`.
diff --git a/doc/licenses.md b/doc/licenses.md
new file mode 100644
index 000000000..805f16bf2
--- /dev/null
+++ b/doc/licenses.md
@@ -0,0 +1,101 @@
+# gitbucket-licenses
+
+Category | License | Dependency | Notes
+--- | --- | --- | ---
+Apache | [ Apache License, Version 2.0 ]( http://opensource.org/licenses/apache2.0.php ) | org.osgi # org.osgi.core # 4.3.1 |
${urlLink(request.fileContent)}""")
}
}
@@ -51,4 +47,4 @@ case class RenderRequest(
enableRefsLink: Boolean,
enableAnchor: Boolean,
context: Context
-)
\ No newline at end of file
+)
diff --git a/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala b/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala
index 3aafa11d3..e7d49df5c 100644
--- a/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala
+++ b/src/main/scala/gitbucket/core/plugin/SuggestionProvider.scala
@@ -3,15 +3,92 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
+/**
+ * The base trait of suggestion providers which supplies completion proposals in some text areas.
+ */
trait SuggestionProvider {
+ /**
+ * The identifier of this suggestion provider.
+ * You must specify the unique identifier in the all suggestion providers.
+ */
val id: String
+
+ /**
+ * The trigger of this suggestion provider. When user types this character, the proposal list would be displayed.
+ * Also this is used as the prefix of the replaced string.
+ */
val prefix: String
+
+ /**
+ * The suffix of the replaced string. The default is `" "`.
+ */
val suffix: String = " "
+
+ /**
+ * Which contexts is this suggestion provider enabled. Currently, available contexts are `"issues"` and `"wiki"`.
+ */
val context: Seq[String]
- def values(repository: RepositoryInfo): Seq[String]
- def template(implicit context: Context): String = "value"
+ /**
+ * If this suggestion provider has static proposal list, override this method to return it.
+ *
+ * The returned sequence is rendered as follows:
+ *
+ * [
+ * {
+ * "label" -> "value1",
+ * "value" -> "value1"
+ * },
+ * {
+ * "label" -> "value2",
+ * "value" -> "value2"
+ * },
+ * ]
+ *
+ *
+ * Each element can be accessed as `option` in `template()` or `replace()` method.
+ */
+ def values(repository: RepositoryInfo): Seq[String] = Nil
+
+ /**
+ * If this suggestion provider has static proposal list, override this method to return it.
+ *
+ * If your proposals have label and value, use this method instead of `values()`.
+ * The first element of tuple is used as a value, and the second element is used as a label.
+ *
+ * The returned sequence is rendered as follows:
+ *
+ * [
+ * {
+ * "label" -> "label1",
+ * "value" -> "value1"
+ * },
+ * {
+ * "label" -> "label2",
+ * "value" -> "value2"
+ * },
+ * ]
+ *
+ *
+ * Each element can be accessed as `option` in `template()` or `replace()` method.
+ */
+ def options(repository: RepositoryInfo): Seq[(String, String)] = values(repository).map { value => (value, value) }
+
+ /**
+ * JavaScript fragment to generate a label of completion proposal. The default is: `option.label`.
+ */
+ def template(implicit context: Context): String = "option.label"
+
+ /**
+ * JavaScript fragment to generate a replaced value of completion proposal. The default is: `option.value`
+ */
+ def replace(implicit context: Context): String = "option.value"
+
+ /**
+ * If this suggestion provider needs some additional process to assemble the proposal list (e.g. It need to use Ajax
+ * to get a proposal list from the server), then override this method and return any JavaScript code.
+ */
def additionalScript(implicit context: Context): String = ""
}
@@ -20,8 +97,6 @@ class UserNameSuggestionProvider extends SuggestionProvider {
override val id: String = "user"
override val prefix: String = "@"
override val context: Seq[String] = Seq("issues")
- override def values(repository: RepositoryInfo): Seq[String] = Nil
- override def template(implicit context: Context): String = "'@' + value"
override def additionalScript(implicit context: Context): String =
s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });"""
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/gitbucket/core/service/ActivityService.scala b/src/main/scala/gitbucket/core/service/ActivityService.scala
index 75b73edd5..2909761d8 100644
--- a/src/main/scala/gitbucket/core/service/ActivityService.scala
+++ b/src/main/scala/gitbucket/core/service/ActivityService.scala
@@ -132,7 +132,7 @@ trait ActivityService {
Activities insert Activity(userName, repositoryName, activityUserName,
"push",
s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
- Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
+ Some(commits.take(5).map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")),
currentDate)
def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String,
diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
index aaa4cf7d3..3ef52e69d 100644
--- a/src/main/scala/gitbucket/core/service/HandleCommentService.scala
+++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
@@ -2,11 +2,10 @@ package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
-import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
-import gitbucket.core.util.Notifier
trait HandleCommentService {
self: RepositoryService with IssuesService with ActivityService
@@ -21,7 +20,7 @@ trait HandleCommentService {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = loginAccount.userName
- val (action, recordActivity) = actionOpt
+ val (action, actionActivity) = actionOpt
.collect {
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
@@ -36,54 +35,55 @@ trait HandleCommentService {
val commentId = (content, action) match {
case (None, None) => None
- case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
- case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
+ case (None, Some(action)) =>
+ Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
+ case (Some(content), _) =>
+ val id = Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
+
+ // record comment activity
+ if(issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content)
+ else recordCommentIssueActivity(owner, name, userName, issue.issueId, content)
+
+ // extract references and create refer comment
+ createReferComment(owner, name, issue, content, loginAccount)
+
+ id
}
- // record comment activity if comment is entered
- content foreach {
- (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
- (owner, name, userName, issue.issueId, _)
- }
- recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
-
- // extract references and create refer comment
- content.map { content =>
- createReferComment(owner, name, issue, content, loginAccount)
- }
+ actionActivity.foreach { f => f(owner, name, userName, issue.issueId, issue.title) }
// call web hooks
action match {
- case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, loginAccount) }
- case Some(act) => {
+ case None => commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount))
+ case Some(act) =>
val webHookAction = act match {
- case "open" => "opened"
- case "reopen" => "reopened"
case "close" => "closed"
- case _ => act
+ case "reopen" => "reopened"
}
- if (issue.isPullRequest) {
+ if(issue.isPullRequest)
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, loginAccount)
- } else {
+ else
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, loginAccount)
- }
- }
}
- // notifications
- Notifier() match {
- case f =>
- content foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
- if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
- }
- }
- action foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
- }
- }
+ // call hooks
+ content foreach { x =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
+ }
+ action foreach {
+ case "close" =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.closed(issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.closed(issue, repository))
+ case "reopen" =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.reopened(issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.reopened(issue, repository))
}
commentId.map( issue -> _ )
diff --git a/src/main/scala/gitbucket/core/service/IssueCreationService.scala b/src/main/scala/gitbucket/core/service/IssueCreationService.scala
index a18dad392..ad6726682 100644
--- a/src/main/scala/gitbucket/core/service/IssueCreationService.scala
+++ b/src/main/scala/gitbucket/core/service/IssueCreationService.scala
@@ -3,17 +3,16 @@ package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.RepositoryService.RepositoryInfo
-import gitbucket.core.util.Notifier
import gitbucket.core.util.Implicits._
-// TODO: Merged with IssuesService?
trait IssueCreationService {
self: RepositoryService with WebHookIssueCommentService with LabelsService with IssuesService with ActivityService =>
def createIssue(repository: RepositoryInfo, title:String, body:Option[String],
- assignee: Option[String], milestoneId: Option[Int], labelNames: Seq[String],
+ assignee: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Seq[String],
loginAccount: Account)(implicit context: Context, s: Session) : Issue = {
val owner = repository.owner
@@ -24,7 +23,8 @@ trait IssueCreationService {
// insert issue
val issueId = insertIssue(owner, name, userName, title, body,
if (manageable) assignee else None,
- if (manageable) milestoneId else None)
+ if (manageable) milestoneId else None,
+ if (manageable) priorityId else None)
val issue: Issue = getIssue(owner, name, issueId.toString).get
// insert labels
@@ -46,10 +46,9 @@ trait IssueCreationService {
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, loginAccount)
- // notifications
- Notifier().toNotify(repository, issue, body.getOrElse("")) {
- Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
- }
+ // call hooks
+ PluginRegistry().getIssueHooks.foreach(_.created(issue, repository))
+
issue
}
diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala
index 4a9b12c2d..5d72a332b 100644
--- a/src/main/scala/gitbucket/core/service/IssuesService.scala
+++ b/src/main/scala/gitbucket/core/service/IssuesService.scala
@@ -32,8 +32,11 @@ trait IssuesService {
.list
def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = {
- getCommentsForApi(owner, repository, issueId)
- .collectFirst { case (comment, account, _) if comment.action == "merged" => (comment, account) }
+ IssueComments.filter(_.byIssue(owner, repository, issueId))
+ .filter(_.action === "merge".bind)
+ .join(Accounts).on { case t1 ~ t2 => t1.commentedUserName === t2.userName }
+ .map { case t1 ~ t2 => (t1, t2)}
+ .firstOption
}
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session): Option[IssueComment] = {
@@ -97,6 +100,30 @@ trait IssuesService {
.list.toMap
}
+ /**
+ * Returns the Map which contains issue count for each priority.
+ *
+ * @param owner the repository owner
+ * @param repository the repository name
+ * @param condition the search condition
+ * @return the Map which contains issue count for each priority (key is priority name, value is issue count)
+ */
+ def countIssueGroupByPriorities(owner: String, repository: String, condition: IssueSearchCondition,
+ filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
+
+ searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
+ .join(Priorities).on { case t1 ~ t2 =>
+ t1.byPriority(t2.userName, t2.repositoryName, t2.priorityId)
+ }
+ .groupBy { case t1 ~ t2 =>
+ t2.priorityName
+ }
+ .map { case priorityName ~ t =>
+ priorityName -> t.length
+ }
+ .list.toMap
+ }
+
def getCommitStatues(userName: String, repositoryName: String, issueId: Int)(implicit s: Session): Option[CommitStatusInfo] = {
val status = PullRequests
.filter { pr =>
@@ -136,21 +163,23 @@ trait IssuesService {
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
- .joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
- .joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) }
- .joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
- .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
- .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 =>
- (t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title))
+ .joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
+ .joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) }
+ .joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
+ .joinLeft (Priorities) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
+ .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
+ .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 =>
+ (t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title), t6.map(_.priorityName))
}
.list
.splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId }
result.map { issues => issues.head match {
- case (issue, commentCount, _, _, _, milestone) =>
+ case (issue, commentCount, _, _, _, milestone, priority) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))} toList,
milestone,
+ priority,
commentCount,
getCommitStatues(issue.userName, issue.repositoryName, issue.issueId))
}} toList
@@ -173,15 +202,16 @@ trait IssuesService {
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
- (implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
+ (implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
- .join(PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
- .join(Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
- .join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
- .join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
- .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
- .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => (t1, t5, t2.commentCount, t3, t4, t6) }
+ .join (PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
+ .join (Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
+ .join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
+ .join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
+ .joinLeft(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName}
+ .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
+ .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list
}
@@ -204,6 +234,10 @@ trait IssuesService {
case "asc" => t1.updatedDate asc
case "desc" => t1.updatedDate desc
}
+ case "priority" => condition.direction match {
+ case "asc" => t2.priority asc
+ case "desc" => t2.priority desc
+ }
}
}
.drop(offset).take(limit).zipWithIndex
@@ -219,6 +253,7 @@ trait IssuesService {
.foldLeft[Rep[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) &&
+ (t1.priorityId.? isEmpty, condition.priority == Some(None)) &&
(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) &&
@@ -227,6 +262,11 @@ trait IssuesService {
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
(t2.title === condition.milestone.get.get.bind)
} exists, condition.milestone.flatten.isDefined) &&
+ // Priority filter
+ (Priorities filter { t2 =>
+ (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) &&
+ (t2.priorityName === condition.priority.get.get.bind)
+ } exists, condition.priority.flatten.isDefined) &&
// Assignee filter
(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) &&
// Label filter
@@ -253,7 +293,7 @@ trait IssuesService {
}
def insertIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
- assignedUserName: Option[String], milestoneId: Option[Int],
+ assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int],
isPullRequest: Boolean = false)(implicit s: Session): Int = {
// next id number
sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int]
@@ -264,6 +304,7 @@ trait IssuesService {
id,
loginUser,
milestoneId,
+ priorityId,
assignedUserName,
title,
content,
@@ -290,6 +331,7 @@ trait IssuesService {
def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String)(implicit s: Session): Int = {
+ Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner,
repositoryName = repository,
@@ -305,27 +347,40 @@ trait IssuesService {
Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => (t.title, t.content.?, t.updatedDate) }
- .update (title, content, currentDate)
+ .update(title, content, currentDate)
}
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String])(implicit s: Session): Int = {
- Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
+ Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.assignedUserName?, t.updatedDate)).update(assignedUserName, currentDate)
}
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = {
- Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
+ Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.milestoneId?, t.updatedDate)).update(milestoneId, currentDate)
}
- def updateComment(commentId: Int, content: String)(implicit s: Session): Int = {
- IssueComments.filter (_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
+ def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = {
+ Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.priorityId?, t.updatedDate)).update(priorityId, currentDate)
}
- def deleteComment(commentId: Int)(implicit s: Session): Int = {
- IssueComments filter (_.byPrimaryKey(commentId)) delete
+ def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = {
+ Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
+ IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
+ }
+
+ def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = {
+ Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
+ IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
+ case Some(c) if c.action == "reopen_comment" =>
+ IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Reopen", "reopen")
+ case Some(c) if c.action == "close_comment" =>
+ IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
+ case Some(_) =>
+ IssueComments.filter(_.byPrimaryKey(commentId)).delete
+ }
}
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session): Int = {
- (Issues filter (_.byPrimaryKey(owner, repository, issueId)) map(t => (t.closed, t.updatedDate))).update((closed, currentDate))
+ Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.closed, t.updatedDate)).update(closed, currentDate)
}
/**
@@ -408,9 +463,8 @@ trait IssuesService {
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = {
extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
- getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
- createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
- }
+ val userName = getAccountByMailAddress(commit.committerEmailAddress).map(_.userName).getOrElse(commit.committerName)
+ createComment(owner, repository, userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
@@ -430,6 +484,7 @@ object IssuesService {
case class IssueSearchCondition(
labels: Set[String] = Set.empty,
milestone: Option[Option[String]] = None,
+ priority: Option[Option[String]] = None,
author: Option[String] = None,
assigned: Option[Option[String]] = None,
mentioned: Option[String] = None,
@@ -455,10 +510,14 @@ object IssuesService {
).flatten ++
labels.map(label => s"label:${label}") ++
List(
- milestone.map { _ match {
+ milestone.map {
case Some(x) => s"milestone:${x}"
case None => "no:milestone"
- }},
+ },
+ priority.map {
+ case Some(x) => s"priority:${x}"
+ case None => "no:priority"
+ },
(sort, direction) match {
case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc")
@@ -466,6 +525,8 @@ object IssuesService {
case ("comments", "asc" ) => Some("sort:comments-asc")
case ("updated" , "desc") => Some("sort:updated-desc")
case ("updated" , "asc" ) => Some("sort:updated-asc")
+ case ("priority", "desc") => Some("sort:priority-desc")
+ case ("priority", "asc" ) => Some("sort:priority-asc")
case x => throw new MatchError(x)
},
visibility.map(visibility => s"visibility:${visibility}")
@@ -480,6 +541,10 @@ object IssuesService {
case Some(x) => "milestone=" + urlEncode(x)
case None => "milestone=none"
},
+ priority.map {
+ case Some(x) => "priority=" + urlEncode(x)
+ case None => "priority=none"
+ },
author .map(x => "author=" + urlEncode(x)),
assigned.map {
case Some(x) => "assigned=" + urlEncode(x)
@@ -512,6 +577,10 @@ object IssuesService {
case "none" => None
case x => Some(x)
},
+ param(request, "priority").map {
+ case "none" => None
+ case x => Some(x)
+ },
param(request, "author"),
param(request, "assigned").map {
case "none" => None
@@ -519,7 +588,7 @@ object IssuesService {
},
param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"),
- param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
+ param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
param(request, "visibility"),
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
@@ -535,6 +604,6 @@ object IssuesService {
case class CommitStatusInfo(count: Int, successCount: Int, context: Option[String], state: Option[CommitState], targetUrl: Option[String], description: Option[String])
- case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
+ case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], priority: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
}
diff --git a/src/main/scala/gitbucket/core/service/MergeService.scala b/src/main/scala/gitbucket/core/service/MergeService.scala
index a58130059..061b2a61e 100644
--- a/src/main/scala/gitbucket/core/service/MergeService.scala
+++ b/src/main/scala/gitbucket/core/service/MergeService.scala
@@ -3,40 +3,55 @@ package gitbucket.core.service
import gitbucket.core.model.Account
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
-
-import org.eclipse.jgit.merge.MergeStrategy
-import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
+import org.eclipse.jgit.api.{Git, MergeResult}
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException
-import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
-import org.eclipse.jgit.revwalk.RevWalk
+import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
+import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
+
+import scala.collection.JavaConverters._
trait MergeService {
import MergeService._
+
/**
* Checks whether conflict will be caused in merging within pull request.
* Returns true if conflict will be caused.
*/
- def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = {
+ def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
- MergeCacheInfo(git, branch, issueId).checkConflict()
+ new MergeCacheInfo(git, branch, issueId).checkConflict()
}
}
+
/**
* Checks whether conflict will be caused in merging within pull request.
* only cache check.
* Returns Some(true) if conflict will be caused.
* Returns None if cache has not created yet.
*/
- def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = {
+ def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Option[String]] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
- MergeCacheInfo(git, branch, issueId).checkConflictCache()
+ new MergeCacheInfo(git, branch, issueId).checkConflictCache()
}
}
- /** merge pull request */
- def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = {
- MergeCacheInfo(git, branch, issueId).merge(message, committer)
+
+ /** merge the pull request with a merge commit */
+ def mergePullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
+ new MergeCacheInfo(git, branch, issueId).merge(message, committer)
}
+
+ /** rebase to the head of the pull request branch */
+ def rebasePullRequest(git: Git, branch: String, issueId: Int, commits: Seq[RevCommit], committer: PersonIdent): Unit = {
+ new MergeCacheInfo(git, branch, issueId).rebase(committer, commits)
+ }
+
+ /** squash commits in the pull request and append it */
+ def squashPullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = {
+ new MergeCacheInfo(git, branch, issueId).squash(message, committer)
+ }
+
/** fetch remote branch to my repository refs/pull/{issueId}/head */
def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){
using(Git.open(getRepositoryDir(userName, repositoryName))){ git =>
@@ -46,11 +61,12 @@ trait MergeService {
.call
}
}
+
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
- remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
+ remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Either[String, (ObjectId, ObjectId, ObjectId)] = {
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${remoteBranch}"
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
@@ -67,12 +83,12 @@ trait MergeService {
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
if(merger.merge(mergeBaseTip, mergeTip)){
- Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
+ Right((merger.getResultTreeId, mergeBaseTip, mergeTip))
} else {
- None
+ Left(createConflictMessage(mergeTip, mergeBaseTip, merger))
}
} catch {
- case e: NoMergeBaseException => None
+ case e: NoMergeBaseException => Left(e.toString)
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
@@ -81,30 +97,33 @@ trait MergeService {
}
}
}
+
/**
- * Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
+ * Checks whether conflict will be caused in merging. Returns `Some(errorMessage)` if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
- requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
- tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
+ requestUserName: String, requestRepositoryName: String, requestBranch: String): Option[String] =
+ tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).left.toOption
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
loginAccount: Account, message: String): Option[ObjectId] = {
- tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
+ tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map { case (newTreeId, oldBaseId, oldHeadId) =>
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
}
oldBaseId
- }
+ }.toOption
}
}
+
object MergeService{
+
object Util{
- // return treeId
+ // return merge commit id
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
@@ -113,14 +132,14 @@ object MergeService{
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
- val inserter = repository.newObjectInserter
- val mergeCommitId = inserter.insert(mergeCommit)
- inserter.flush()
- inserter.close()
- mergeCommitId
+ using(repository.newObjectInserter){ inserter =>
+ val mergeCommitId = inserter.insert(mergeCommit)
+ inserter.flush()
+ mergeCommitId
+ }
}
- def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
- // update refs
+
+ def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None): Unit = {
val refUpdate = repository.updateRef(ref)
refUpdate.setNewObjectId(newObjectId)
refUpdate.setForceUpdate(force)
@@ -129,33 +148,41 @@ object MergeService{
refUpdate.update()
}
}
- case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
- val repository = git.getRepository
- val mergedBranchName = s"refs/pull/${issueId}/merge"
- val conflictedBranchName = s"refs/pull/${issueId}/conflict"
+
+ class MergeCacheInfo(git: Git, branch: String, issueId: Int){
+
+ private val repository = git.getRepository
+
+ private val mergedBranchName = s"refs/pull/${issueId}/merge"
+ private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
+
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
- lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
- def checkConflictCache(): Option[Boolean] = {
- Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
+ lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
+
+ def checkConflictCache(): Option[Option[String]] = {
+ Option(repository.resolve(mergedBranchName)).flatMap { merged =>
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists
- Some(false)
+ Some(None)
} else {
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
- if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
+ val commit = parseCommit(conflicted)
+ if(commit.getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
- Some(true)
+ Some(Some(commit.getFullMessage))
} else {
None
}
})
}
- def checkConflict():Boolean ={
+
+ def checkConflict(): Option[String] ={
checkConflictCache.getOrElse(checkConflictForce)
}
- def checkConflictForce():Boolean ={
+
+ def checkConflictForce(): Option[String] ={
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
@@ -163,36 +190,114 @@ object MergeService{
case e: NoMergeBaseException => true
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
- val committer = mergeTipCommit.getCommitterIdent;
- def updateBranch(treeId:ObjectId, message:String, branchName:String){
+ val committer = mergeTipCommit.getCommitterIdent
+
+ def _updateBranch(treeId: ObjectId, message: String, branchName: String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
}
+
if(!conflicted){
- updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
+ _updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
+ None
} else {
- updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
+ val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
+ _updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
+ Some(message)
}
- conflicted
}
+
// update branch from cache
def merge(message:String, committer:PersonIdent) = {
- if(checkConflict()){
+ if(checkConflict().isDefined){
throw new RuntimeException("This pull request can't merge automatically.")
}
- val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) )
+ val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse {
+ throw new RuntimeException(s"Not found branch ${mergedBranchName}")
+ })
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
// update refs
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
}
+
+ def rebase(committer: PersonIdent, commits: Seq[RevCommit]): Unit = {
+ if(checkConflict().isDefined){
+ throw new RuntimeException("This pull request can't merge automatically.")
+ }
+
+ def _cloneCommit(commit: RevCommit, parents: Array[ObjectId]): CommitBuilder = {
+ val newCommit = new CommitBuilder()
+ newCommit.setTreeId(commit.getTree.getId)
+ parents.foreach { parentId =>
+ newCommit.addParentId(parentId)
+ }
+ newCommit.setAuthor(commit.getAuthorIdent)
+ newCommit.setCommitter(committer)
+ newCommit.setMessage(commit.getFullMessage)
+ newCommit
+ }
+
+ val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeBaseTip ))
+ var previousId = mergeBaseTipCommit.getId
+
+ using(repository.newObjectInserter){ inserter =>
+ commits.foreach { commit =>
+ val nextCommit = _cloneCommit(commit, Array(previousId))
+ previousId = inserter.insert(nextCommit)
+ }
+ inserter.flush()
+ }
+
+ Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased"))
+ }
+
+ def squash(message: String, committer: PersonIdent): Unit = {
+ if(checkConflict().isDefined){
+ throw new RuntimeException("This pull request can't merge automatically.")
+ }
+
+ val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit(mergeBaseTip))
+ val mergeBranchHeadCommit = using(new RevWalk( repository ))(_.parseCommit(repository.resolve(mergedBranchName)))
+
+ // Create squash commit
+ val mergeCommit = new CommitBuilder()
+ mergeCommit.setTreeId(mergeBranchHeadCommit.getTree.getId)
+ mergeCommit.setParentId(mergeBaseTipCommit)
+ mergeCommit.setAuthor(mergeBranchHeadCommit.getAuthorIdent)
+ mergeCommit.setCommitter(committer)
+ mergeCommit.setMessage(message)
+
+ // insertObject and got squash commit Object Id
+ val newCommitId = using(repository.newObjectInserter){ inserter =>
+ val newCommitId = inserter.insert(mergeCommit)
+ inserter.flush()
+ newCommitId
+ }
+
+ Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer)
+
+ // rebase to squash commit
+ Util.updateRefs(repository, s"refs/heads/${branch}", repository.resolve(mergedBranchName), false, committer, Some("squashed"))
+ }
+
// return treeId
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
- private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
+ private def parseCommit(id: ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
+
}
+
+ private def createConflictMessage(mergeTip: ObjectId, mergeBaseTip: ObjectId, merger: Merger): String = {
+ val mergeResults = merger.asInstanceOf[RecursiveMerger].getMergeResults
+
+ s"Can't merge ${mergeTip.name} into ${mergeBaseTip.name}\n\n" +
+ "Conflicting files:\n" +
+ mergeResults.asScala.map { case (key, _) => "- " + key + "\n" }.mkString
+ }
+
}
diff --git a/src/main/scala/gitbucket/core/service/PrioritiesService.scala b/src/main/scala/gitbucket/core/service/PrioritiesService.scala
new file mode 100644
index 000000000..cafff4b15
--- /dev/null
+++ b/src/main/scala/gitbucket/core/service/PrioritiesService.scala
@@ -0,0 +1,84 @@
+package gitbucket.core.service
+
+import gitbucket.core.model.Priority
+import gitbucket.core.model.Profile._
+import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.util.StringUtil
+
+trait PrioritiesService {
+
+ def getPriorities(owner: String, repository: String)(implicit s: Session): List[Priority] =
+ Priorities.filter(_.byRepository(owner, repository)).sortBy(_.ordering asc).list
+
+ def getPriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Option[Priority] =
+ Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).firstOption
+
+ def getPriority(owner: String, repository: String, priorityName: String)(implicit s: Session): Option[Priority] =
+ Priorities.filter(_.byPriority(owner, repository, priorityName)).firstOption
+
+ def createPriority(owner: String, repository: String, priorityName: String, description: Option[String], color: String)(implicit s: Session): Int = {
+ val ordering = Priorities.filter(_.byRepository(owner, repository))
+ .list
+ .map(p => p.ordering)
+ .reduceOption(_ max _)
+ .map(m => m + 1)
+ .getOrElse(0)
+
+ Priorities returning Priorities.map(_.priorityId) insert Priority(
+ userName = owner,
+ repositoryName = repository,
+ priorityName = priorityName,
+ description = description,
+ isDefault = false,
+ ordering = ordering,
+ color = color
+ )
+ }
+
+ def updatePriority(owner: String, repository: String, priorityId: Int, priorityName: String, description: Option[String], color: String)
+ (implicit s: Session): Unit =
+ Priorities.filter(_.byPrimaryKey(owner, repository, priorityId))
+ .map(t => (t.priorityName, t.description.?, t.color))
+ .update(priorityName, description, color)
+
+ def reorderPriorities(owner: String, repository: String, order: Map[Int, Int])
+ (implicit s: Session): Unit = {
+
+ Priorities.filter(_.byRepository(owner, repository))
+ .list
+ .foreach(p => Priorities
+ .filter(_.byPrimaryKey(owner, repository, p.priorityId))
+ .map(_.ordering)
+ .update(order.get(p.priorityId).get))
+ }
+
+ def deletePriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Unit = {
+ Issues.filter(_.byRepository(owner, repository))
+ .filter(_.priorityId === priorityId)
+ .map(_.priorityId?)
+ .update(None)
+
+ Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).delete
+ }
+
+ def getDefaultPriority(owner: String, repository: String)(implicit s: Session): Option[Priority] = {
+ Priorities
+ .filter(_.byRepository(owner, repository))
+ .filter(_.isDefault)
+ .list
+ .headOption
+ }
+
+ def setDefaultPriority(owner: String, repository: String, priorityId: Option[Int])(implicit s: Session): Unit = {
+ Priorities
+ .filter(_.byRepository(owner, repository))
+ .filter(_.isDefault)
+ .map(_.isDefault)
+ .update(false)
+
+ priorityId.foreach(id => Priorities
+ .filter(_.byPrimaryKey(owner, repository, id))
+ .map(_.isDefault)
+ .update(true))
+ }
+}
diff --git a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala
index d74280cf7..65d0aaa6a 100644
--- a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala
+++ b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala
@@ -1,11 +1,10 @@
package gitbucket.core.service
-import gitbucket.core.model.{ProtectedBranch, ProtectedBranchContext, CommitState}
+import gitbucket.core.model.{Session => _, _}
import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
-
-import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
+import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
trait ProtectedBranchService {
@@ -18,10 +17,11 @@ trait ProtectedBranchService {
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
+ .headOption
.map { p => p._1 -> p._2.flatMap(_._2) }
.map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
- }.headOption
+ }
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
@@ -45,12 +45,17 @@ trait ProtectedBranchService {
object ProtectedBranchService {
- class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
+ class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService with RepositoryService with AccountService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){
- getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
+ val repositoryInfo = getRepository(owner, repository)
+ if(command.getType == ReceiveCommand.Type.DELETE && repositoryInfo.exists(_.repository.defaultBranch == branch)){
+ Some(s"refusing to delete the branch: ${command.getRefName}.")
+ } else {
+ getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
+ }
} else {
None
}
@@ -73,10 +78,19 @@ object ProtectedBranchService {
* Include administrators
* Enforce required status checks for repository administrators.
*/
- includeAdministrators: Boolean) extends AccountService with CommitStatusService {
+ includeAdministrators: Boolean) extends AccountService with RepositoryService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
- pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager)
+ pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager) ||
+ getCollaborators(owner, repository).exists { case (collaborator, isGroup) =>
+ if(collaborator.role == Role.ADMIN.name){
+ if(isGroup){
+ getGroupMembers(collaborator.collaboratorName).exists(gm => gm.userName == pusher)
+ } else {
+ collaborator.collaboratorName == pusher
+ }
+ } else false
+ }
/**
* Can't be force pushed
diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala
index 03f2e6ed7..b6c009a52 100644
--- a/src/main/scala/gitbucket/core/service/PullRequestService.scala
+++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala
@@ -79,7 +79,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
commitIdFrom,
commitIdTo)
- def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean)
+ def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Option[Boolean])
(implicit s: Session): List[PullRequest] =
PullRequests
.join(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
@@ -87,16 +87,16 @@ trait PullRequestService { self: IssuesService with CommitsService =>
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === branch.bind) &&
- (t2.closed === closed.bind)
+ (t2.closed === closed.get.bind, closed.isDefined)
}
.map { case (t1, t2) => t1 }
.list
/**
* for repository viewer.
- * 1. find pull request from from `branch` to othre branch on same repository
+ * 1. find pull request from `branch` to other branch on same repository
* 1. return if exists pull request to `defaultBranch`
- * 2. return if exists pull request to othre branch
+ * 2. return if exists pull request to other branch
* 2. return None
*/
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
@@ -118,7 +118,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
- getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
+ getPullRequestsByRequest(owner, repository, branch, Some(false)).foreach { pullreq =>
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
// Update the git repository
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
- val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
+ val diffs = JGitUtil.getDiffs(newGit, Some(oldId.getName), newId.getName, true, false)
(commits, diffs)
}
@@ -244,8 +244,8 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int)
case class MergeStatus(
- hasConflict: Boolean,
- commitStatues:List[CommitStatus],
+ conflictMessage: Option[String],
+ commitStatues: List[CommitStatus],
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
branchIsOutOfDate: Boolean,
hasUpdatePermission: Boolean,
@@ -253,12 +253,13 @@ object PullRequestService {
hasMergePermission: Boolean,
commitIdTo: String){
+ val hasConflict = conflictMessage.isDefined
val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
- val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
- val canUpdate = branchIsOutOfDate && !hasConflict
- val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
+ val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
+ val canUpdate = branchIsOutOfDate && !hasConflict
+ val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala
index 7381bbc52..7b9bffd79 100644
--- a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala
@@ -1,68 +1,206 @@
package gitbucket.core.service
+import java.nio.file.Files
+import java.util.concurrent.ConcurrentHashMap
+
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
-import gitbucket.core.util.JGitUtil
-import gitbucket.core.model.Account
+import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil}
+import gitbucket.core.model.{Account, Role}
+import gitbucket.core.plugin.PluginRegistry
+import gitbucket.core.service.RepositoryService.RepositoryInfo
+import gitbucket.core.servlet.Database
+import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
-import org.eclipse.jgit.lib.{FileMode, Constants}
+import org.eclipse.jgit.lib.{Constants, FileMode}
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+
+object RepositoryCreationService {
+
+ private val Creating = new ConcurrentHashMap[String, Option[String]]()
+
+ def isCreating(owner: String, repository: String): Boolean = {
+ Option(Creating.get(s"${owner}/${repository}")).map(_.isEmpty).getOrElse(false)
+ }
+
+ def startCreation(owner: String, repository: String): Unit = {
+ Creating.put(s"${owner}/${repository}", None)
+ }
+
+ def endCreation(owner: String, repository: String, error: Option[String]): Unit = {
+ error match {
+ case None => Creating.remove(s"${owner}/${repository}")
+ case Some(error) => Creating.put(s"${owner}/${repository}", Some(error))
+ }
+ }
+
+ def getCreationError(owner: String, repository: String): Option[String] = {
+ Option(Creating.remove(s"${owner}/${repository}")).getOrElse(None)
+ }
+
+}
trait RepositoryCreationService {
- self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
+ self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService with PrioritiesService =>
- def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
- (implicit s: Session) {
- val ownerAccount = getAccountByUserName(owner).get
- val loginUserName = loginAccount.userName
+ def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
+ isPrivate: Boolean, createReadme: Boolean): Future[Unit] = {
+ createRepository(loginAccount, owner, name, description, isPrivate, if (createReadme) "README" else "EMPTY", None)
+ }
- // Insert to the database at first
- insertRepository(name, owner, description, isPrivate)
+ def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String],
+ isPrivate: Boolean, initOption: String, sourceUrl: Option[String]): Future[Unit] = Future {
+ RepositoryCreationService.startCreation(owner, name)
+ try {
+ Database() withTransaction { implicit session =>
+ val ownerAccount = getAccountByUserName(owner).get
+ val loginUserName = loginAccount.userName
-// // Add collaborators for group repository
-// if(ownerAccount.isGroupAccount){
-// getGroupMembers(owner).foreach { member =>
-// addCollaborator(owner, name, member.userName)
-// }
-// }
+ val copyRepositoryDir = if (initOption == "COPY") {
+ sourceUrl.flatMap { url =>
+ val dir = Files.createTempDirectory(s"gitbucket-${owner}-${name}").toFile
+ Git.cloneRepository().setBare(true).setURI(url).setDirectory(dir).setCloneAllBranches(true).call()
+ Some(dir)
+ }
+ } else None
- // Insert default labels
- insertDefaultLabels(owner, name)
- // Create the actual repository
- val gitdir = getRepositoryDir(owner, name)
- JGitUtil.initRepository(gitdir)
+ // Insert to the database at first
+ insertRepository(name, owner, description, isPrivate)
- if(createReadme){
- using(Git.open(gitdir)){ git =>
- val builder = DirCache.newInCore.builder()
- val inserter = git.getRepository.newObjectInserter()
- val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
- val content = if(description.nonEmpty){
- name + "\n" +
- "===============\n" +
- "\n" +
- description.get
- } else {
- name + "\n" +
- "===============\n"
+ // // Add collaborators for group repository
+ // if(ownerAccount.isGroupAccount){
+ // getGroupMembers(owner).foreach { member =>
+ // addCollaborator(owner, name, member.userName)
+ // }
+ // }
+
+ // Insert default labels
+ insertDefaultLabels(owner, name)
+
+ // Insert default priorities
+ insertDefaultPriorities(owner, name)
+
+ // Create the actual repository
+ val gitdir = getRepositoryDir(owner, name)
+ JGitUtil.initRepository(gitdir)
+
+ if (initOption == "README") {
+ using(Git.open(gitdir)) { git =>
+ val builder = DirCache.newInCore.builder()
+ val inserter = git.getRepository.newObjectInserter()
+ val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
+ val content = if (description.nonEmpty) {
+ name + "\n" +
+ "===============\n" +
+ "\n" +
+ description.get
+ } else {
+ name + "\n" +
+ "===============\n"
+ }
+
+ builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
+ inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
+ builder.finish()
+
+ JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
+ Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
+ }
}
- builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
- inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
- builder.finish()
+ copyRepositoryDir.foreach { dir =>
+ try {
+ using(Git.open(dir)) { git =>
+ git.push().setRemote(gitdir.toURI.toString).setPushAll().setPushTags().call()
+ }
+ } finally {
+ FileUtils.deleteQuietly(dir)
+ }
+ }
- JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
- Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
+ // Create Wiki repository
+ createWikiRepository(loginAccount, owner, name)
+
+ // Record activity
+ recordCreateRepositoryActivity(owner, name, loginUserName)
+
+ // Call hooks
+ PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name))
}
+
+ RepositoryCreationService.endCreation(owner, name, None)
+
+ } catch {
+ case ex: Exception => RepositoryCreationService.endCreation(owner, name, Some(ex.toString))
}
+ }
- // Create Wiki repository
- createWikiRepository(loginAccount, owner, name)
+ def forkRepository(accountName: String, repository: RepositoryInfo, loginUserName: String): Future[Unit] = Future {
+ RepositoryCreationService.startCreation(accountName, repository.name)
+ try {
+ LockUtil.lock(s"${accountName}/${repository.name}") {
+ Database() withTransaction { implicit session =>
+ val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
+ val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
- // Record activity
- recordCreateRepositoryActivity(owner, name, loginUserName)
+ insertRepository(
+ repositoryName = repository.name,
+ userName = accountName,
+ description = repository.repository.description,
+ isPrivate = repository.repository.isPrivate,
+ originRepositoryName = Some(originRepositoryName),
+ originUserName = Some(originUserName),
+ parentRepositoryName = Some(repository.name),
+ parentUserName = Some(repository.owner)
+ )
+
+ // Set default collaborators for the private fork
+ if (repository.repository.isPrivate) {
+ // Copy collaborators from the source repository
+ getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) =>
+ addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role)
+ }
+ // Register an owner of the source repository as a collaborator
+ addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name)
+ }
+
+ // Insert default labels
+ insertDefaultLabels(accountName, repository.name)
+ // Insert default priorities
+ insertDefaultPriorities(accountName, repository.name)
+
+ // clone repository actually
+ JGitUtil.cloneRepository(
+ getRepositoryDir(repository.owner, repository.name),
+ FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
+
+ // Create Wiki repository
+ JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name),
+ FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name)))
+
+ // Copy LFS files
+ val lfsDir = getLfsDir(repository.owner, repository.name)
+ if (lfsDir.exists) {
+ FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name)))
+ }
+
+ // Record activity
+ recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
+
+ // Call hooks
+ PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
+
+ RepositoryCreationService.endCreation(accountName, repository.name, None)
+ }
+ }
+ } catch {
+ case ex: Exception => RepositoryCreationService.endCreation(accountName, repository.name, Some(ex.toString))
+ }
}
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
@@ -74,5 +212,13 @@ trait RepositoryCreationService {
createLabel(userName, repositoryName, "wontfix", "ffffff")
}
+ def insertDefaultPriorities(userName: String, repositoryName: String)(implicit s: Session): Unit = {
+ createPriority(userName, repositoryName, "highest", Some("All defects at this priority must be fixed before any public product is delivered."), "fc2929")
+ createPriority(userName, repositoryName, "very high", Some("Issues must be addressed before a final product is delivered."), "fc5629")
+ createPriority(userName, repositoryName, "high", Some("Issues should be addressed before a final product is delivered. If the issue cannot be resolved before delivery, it should be prioritized for the next release."), "fc9629")
+ createPriority(userName, repositoryName, "important", Some("Issues can be shipped with a final product, but should be reviewed before the next release."), "fccd29")
+ createPriority(userName, repositoryName, "default", Some("Default."), "acacac")
+ setDefaultPriority(userName, repositoryName, getPriority(userName, repositoryName, "default").map(_.priorityId))
+ }
}
diff --git a/src/main/scala/gitbucket/core/service/RepositorySearchService.scala b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
index d08c7bce6..11be72464 100644
--- a/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
@@ -67,7 +67,7 @@ trait RepositorySearchService { self: IssuesService =>
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
- path.replaceFirst("\\.md$", ""),
+ path.stripSuffix(".md"),
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)
diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala
index 71d40bc95..79e7e47d9 100644
--- a/src/main/scala/gitbucket/core/service/RepositoryService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala
@@ -59,13 +59,14 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
- val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
- val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val webHooks = RepositoryWebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val webHookEvents = RepositoryWebHookEvents.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
+ val priorities = Priorities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
@@ -83,7 +84,7 @@ trait RepositoryService { self: AccountService =>
Repositories.filter { t =>
(t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind)
- }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName)
+ }.map { t => t.parentUserName -> t.parentRepositoryName }.update(newUserName, newRepositoryName)
// Updates activity fk before deleting repository because activity is sorted by activityId
// and it can't be changed by deleting-and-inserting record.
@@ -94,17 +95,22 @@ trait RepositoryService { self: AccountService =>
deleteRepository(oldUserName, oldRepositoryName)
- WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
- IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
+ RepositoryWebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ RepositoryWebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
+ IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
+ val newPriorities = Priorities.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
userName = newUserName,
repositoryName = newRepositoryName,
milestoneId = x.milestoneId.map { id =>
newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId
+ },
+ priorityId = x.priorityId.map { id =>
+ newPriorities.find(_.priorityName == priorities.find(_.priorityId == id).get.priorityName).get.priorityId
}
)} :_*)
@@ -133,7 +139,7 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName
)) :_*)
- // TODO Drop transfered owner from collaborators?
+ // TODO Drop transferred owner from collaborators?
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
@@ -157,22 +163,23 @@ trait RepositoryService { self: AccountService =>
}
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
- Activities .filter(_.byRepository(userName, repositoryName)).delete
- Collaborators .filter(_.byRepository(userName, repositoryName)).delete
- CommitComments.filter(_.byRepository(userName, repositoryName)).delete
- IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
- Labels .filter(_.byRepository(userName, repositoryName)).delete
- IssueComments .filter(_.byRepository(userName, repositoryName)).delete
- PullRequests .filter(_.byRepository(userName, repositoryName)).delete
- Issues .filter(_.byRepository(userName, repositoryName)).delete
- IssueId .filter(_.byRepository(userName, repositoryName)).delete
- Milestones .filter(_.byRepository(userName, repositoryName)).delete
- WebHooks .filter(_.byRepository(userName, repositoryName)).delete
- WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
- DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
+ Activities .filter(_.byRepository(userName, repositoryName)).delete
+ Collaborators .filter(_.byRepository(userName, repositoryName)).delete
+ CommitComments .filter(_.byRepository(userName, repositoryName)).delete
+ IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
+ Labels .filter(_.byRepository(userName, repositoryName)).delete
+ IssueComments .filter(_.byRepository(userName, repositoryName)).delete
+ PullRequests .filter(_.byRepository(userName, repositoryName)).delete
+ Issues .filter(_.byRepository(userName, repositoryName)).delete
+ Priorities .filter(_.byRepository(userName, repositoryName)).delete
+ IssueId .filter(_.byRepository(userName, repositoryName)).delete
+ Milestones .filter(_.byRepository(userName, repositoryName)).delete
+ RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete
+ RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
+ DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
ReleaseAssets .filter(_.byRepository(userName, repositoryName)).delete
Releases .filter(_.byRepository(userName, repositoryName)).delete
- Repositories .filter(_.byRepository(userName, repositoryName)).delete
+ Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
Repositories
@@ -394,7 +401,7 @@ trait RepositoryService { self: AccountService =>
Collaborators
.join(Accounts).on(_.collaboratorName === _.userName)
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
- .map { case (t1, t2) => (t1, t2.groupAccount) }
+ .map { case (t1, t2) => (t1, t2.groupAccount) }
.sortBy { case (t1, t2) => t1.collaboratorName }
.list
@@ -406,17 +413,26 @@ trait RepositoryService { self: AccountService =>
val q1 = Collaborators
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) }
.filter { case (t1, t2) => t1.byRepository(userName, repositoryName) }
- .map { case (t1, t2) => (t1.collaboratorName, t1.role) }
+ .map { case (t1, t2) => (t1.collaboratorName, t1.role) }
val q2 = Collaborators
.join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) }
.join(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName }
.filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) }
- .map { case ((t1, t2), t3) => (t3.userName, t1.role) }
+ .map { case ((t1, t2), t3) => (t3.userName, t1.role) }
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
+ def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
+ loginAccount match {
+ case Some(a) if(a.isAdmin) => true
+ case Some(a) if(a.userName == owner) => true
+ case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
+ case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
+ case _ => false
+ }
+ }
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
@@ -438,17 +454,31 @@ trait RepositoryService { self: AccountService =>
}
}
+ def isReadable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
+ if(!repository.isPrivate){
+ true
+ } else {
+ loginAccount match {
+ case Some(x) if(x.isAdmin) => true
+ case Some(x) if(repository.userName == x.userName) => true
+ case Some(x) if(getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
+ case Some(x) if(getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) => true
+ case _ => false
+ }
+ }
+ }
+
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}.length).first
- def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] =
+ def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[Repository] =
Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
}
- .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list
+ .sortBy(_.userName asc).list//.map(t => t.userName -> t.repositoryName).list
private val templateExtensions = Seq("md", "markdown")
diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
index 1647cba6c..bb50df4e2 100644
--- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
+++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala
@@ -1,9 +1,9 @@
package gitbucket.core.service
-import gitbucket.core.util.{Directory, SyntaxSugars}
import gitbucket.core.util.Implicits._
-import Directory._
-import SyntaxSugars._
+import gitbucket.core.util.ConfigUtil._
+import gitbucket.core.util.Directory._
+import gitbucket.core.util.SyntaxSugars._
import SystemSettingsService._
import javax.servlet.http.HttpServletRequest
@@ -54,6 +54,7 @@ trait SystemSettingsService {
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
+ props.setProperty(SkinName, settings.skinName.toString)
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null)
}
@@ -111,7 +112,8 @@ trait SystemSettingsService {
getOptionValue(props, LdapKeystore, None)))
} else {
None
- }
+ },
+ getValue(props, SkinName, "skin-blue")
)
}
}
@@ -136,18 +138,18 @@ object SystemSettingsService {
useSMTP: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
- ldap: Option[Ldap]){
- def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
+ ldap: Option[Ldap],
+ skinName: String){
- def sshAddress:Option[SshAddress] =
- for {
- host <- sshHost if ssh
- }
- yield SshAddress(
- host,
- sshPort.getOrElse(DefaultSshPort),
- "git"
- )
+ def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
+ val url = request.getRequestURL.toString
+ val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
+ url.substring(0, len).stripSuffix("/")
+ } (_.stripSuffix("/"))
+
+ def sshAddress:Option[SshAddress] = sshHost.collect { case host if ssh =>
+ SshAddress(host, sshPort.getOrElse(DefaultSshPort), "git")
+ }
}
case class Ldap(
@@ -219,24 +221,30 @@ object SystemSettingsService {
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
+ private val SkinName = "skinName"
- private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
- defining(props.getProperty(key)){ value =>
- if(value == null || value.isEmpty) default
- else convertType(value).asInstanceOf[A]
- }
+ private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
+ getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse {
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty){
+ default
+ } else {
+ convertType(value).asInstanceOf[A]
+ }
+ }
+ })
+ }
- private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] =
- defining(props.getProperty(key)){ value =>
- if(value == null || value.isEmpty) default
- else Some(convertType(value)).asInstanceOf[Option[A]]
- }
-
- private def convertType[A: ClassTag](value: String) =
- defining(implicitly[ClassTag[A]].runtimeClass){ c =>
- if(c == classOf[Boolean]) value.toBoolean
- else if(c == classOf[Int]) value.toInt
- else value
- }
+ private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = {
+ getSystemProperty(key).orElse(getEnvironmentVariable(key).orElse {
+ defining(props.getProperty(key)){ value =>
+ if(value == null || value.isEmpty){
+ default
+ } else {
+ Some(convertType(value)).asInstanceOf[Option[A]]
+ }
+ }
+ })
+ }
}
diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala
index 2f060de6f..f7a4e071a 100644
--- a/src/main/scala/gitbucket/core/service/WebHookService.scala
+++ b/src/main/scala/gitbucket/core/service/WebHookService.scala
@@ -3,12 +3,12 @@ package gitbucket.core.service
import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
import gitbucket.core.api._
-import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, WebHookEvent}
+import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, RepositoryWebHook, RepositoryWebHookEvent, AccountWebHook, AccountWebHookEvent}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils
import gitbucket.core.util.JGitUtil.CommitInfo
-import gitbucket.core.util.RepositoryName
+import gitbucket.core.util.{RepositoryName, StringUtil}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
@@ -18,7 +18,7 @@ import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory
import scala.concurrent._
-import scala.util.{Success, Failure}
+import scala.util.{Failure, Success}
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType
@@ -32,45 +32,86 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
/** get All WebHook informations of repository */
- def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] =
- WebHooks.filter(_.byRepository(owner, repository))
- .join(WebHookEvents).on { (w, t) => t.byWebHook(w) }
+ def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(RepositoryWebHook, Set[WebHook.Event])] =
+ RepositoryWebHooks.filter(_.byRepository(owner, repository))
+ .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
/** get All WebHook informations of repository event */
- def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
- WebHooks.filter(_.byRepository(owner, repository))
- .join(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
+ def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[RepositoryWebHook] =
+ RepositoryWebHooks.filter(_.byRepository(owner, repository))
+ .join(RepositoryWebHookEvents).on { (wh, whe) => whe.byRepositoryWebHook(wh) }
.filter { case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh }
.list.distinct
/** get All WebHook information from repository to url */
- def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
- WebHooks
+ def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(RepositoryWebHook, Set[WebHook.Event])] =
+ RepositoryWebHooks
.filter(_.byPrimaryKey(owner, repository, url))
- .join(WebHookEvents).on { (w, t) => t.byWebHook(w) }
+ .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
- WebHooks insert WebHook(owner, repository, url, ctype, token)
+ RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token)
events.map { event: WebHook.Event =>
- WebHookEvents insert WebHookEvent(owner, repository, url, event)
+ RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
}
}
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
- WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
- WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
+ RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
+ RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
events.map { event: WebHook.Event =>
- WebHookEvents insert WebHookEvent(owner, repository, url, event)
+ RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
}
}
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
- WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
+ RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
+
+ /** get All AccountWebHook informations of user */
+ def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] =
+ AccountWebHooks.filter(_.byAccount(owner))
+ .join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
+ .map { case (w, t) => w -> t.event }
+ .list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
+
+ /** get All AccountWebHook informations of repository event */
+ def getAccountWebHooksByEvent(owner: String, event: WebHook.Event)(implicit s: Session): List[AccountWebHook] =
+ AccountWebHooks.filter(_.byAccount(owner))
+ .join(AccountWebHookEvents).on { (wh, whe) => whe.byAccountWebHook(wh) }
+ .filter { case (wh, whe) => whe.event === event.bind}
+ .map{ case (wh, whe) => wh }
+ .list.distinct
+
+ /** get All AccountWebHook information from repository to url */
+ def getAccountWebHook(owner: String, url: String)(implicit s: Session): Option[(AccountWebHook, Set[WebHook.Event])] =
+ AccountWebHooks
+ .filter(_.byPrimaryKey(owner, url))
+ .join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
+ .map { case (w, t) => w -> t.event }
+ .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
+
+ def addAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
+ AccountWebHooks insert AccountWebHook(owner, url, ctype, token)
+ events.map { event: WebHook.Event =>
+ AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
+ }
+ }
+
+ def updateAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
+ AccountWebHooks.filter(_.byPrimaryKey(owner, url)).map(w => (w.ctype, w.token)).update((ctype, token))
+ AccountWebHookEvents.filter(_.byAccountWebHook(owner, url)).delete
+ events.map { event: WebHook.Event =>
+ AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
+ }
+ }
+
+ def deleteAccountWebHook(owner: String, url :String)(implicit s: Session): Unit =
+ AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])
(implicit s: Session, c: JsonFormat.Context): Unit = {
@@ -78,6 +119,10 @@ trait WebHookService {
if(webHooks.nonEmpty){
makePayload.map(callWebHook(event, webHooks, _))
}
+ val accountWebHooks = getAccountWebHooksByEvent(owner, event)
+ if(accountWebHooks.nonEmpty){
+ makePayload.map(callWebHook(event, accountWebHooks, _))
+ }
}
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
@@ -160,7 +205,7 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._
// https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)
- (implicit s: Session, context:JsonFormat.Context): Unit = {
+ (implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues){
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{
@@ -178,21 +223,23 @@ trait WebHookPullRequestService extends WebHookService {
}
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
- (implicit s: Session, context:JsonFormat.Context): Unit = {
+ (implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
- users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
+ users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
+ assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
+ assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -207,7 +254,7 @@ trait WebHookPullRequestService extends WebHookService {
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
- (implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] =
+ (implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[RepositoryWebHook]] =
(for{
is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
@@ -217,23 +264,25 @@ trait WebHookPullRequestService extends WebHookService {
bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName
- wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
- wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh)
+ wh <- RepositoryWebHooks if wh.byRepository(is.userName , is.repositoryName)
+ wht <- RepositoryWebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byRepositoryWebHook(wh)
} yield {
((is, iu, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2))
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)
- (implicit s: Session, context:JsonFormat.Context): Unit = {
+ (implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
+ assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
+ assignee = assignee,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -246,20 +295,22 @@ trait WebHookPullRequestService extends WebHookService {
callWebHook(WebHook.PullRequest, webHooks, payload)
}
}
+
}
trait WebHookPullRequestReviewCommentService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
- def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
- (implicit s: Session, context:JsonFormat.Context): Unit = {
+ def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo,
+ issue: Issue, pullRequest: PullRequest, baseUrl: String, sender: Account)
+ (implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
+ val users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for{
- (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
- users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
+ assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestReviewCommentPayload(
@@ -267,6 +318,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
+ assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -285,7 +337,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
import WebHookService._
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)
- (implicit s: Session, context:JsonFormat.Context): Unit = {
+ (implicit s: Session, c: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
@@ -310,6 +362,35 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
object WebHookService {
trait WebHookPayload
+ // https://developer.github.com/v3/activity/events/types/#createevent
+ case class WebHookCreatePayload(
+ sender: ApiUser,
+ description: String,
+ ref: String,
+ ref_type: String,
+ master_branch: String,
+ repository: ApiRepository
+ ) extends FieldSerializable with WebHookPayload {
+ val pusher_type = "user"
+ }
+
+ object WebHookCreatePayload {
+
+ def apply(git: Git, sender: Account, refName: String, repositoryInfo: RepositoryInfo,
+ commits: List[CommitInfo], repositoryOwner: Account,
+ ref: String, refType: String): WebHookCreatePayload =
+ WebHookCreatePayload(
+ sender = ApiUser(sender),
+ ref = ref,
+ ref_type = refType,
+ description = repositoryInfo.repository.description.getOrElse(""),
+ master_branch = repositoryInfo.repository.defaultBranch,
+ repository = ApiRepository.forWebhookPayload(
+ repositoryInfo,
+ owner= ApiUser(repositoryOwner))
+ )
+ }
+
// https://developer.github.com/v3/activity/events/types/#pushevent
case class WebHookPushPayload(
pusher: ApiPusher,
@@ -321,9 +402,9 @@ object WebHookService {
repository: ApiRepository
) extends FieldSerializable with WebHookPayload {
val compare = commits.size match {
- case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
+ case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initialized repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
- case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
+ case _ if before.forall(_=='0') => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
}
val head_commit = commits.lastOption
@@ -339,11 +420,22 @@ object WebHookService {
ref = refName,
before = ObjectId.toString(oldId),
after = ObjectId.toString(newId),
- commits = commits.map{ commit => ApiCommit.forPushPayload(git, RepositoryName(repositoryInfo), commit) },
- repository = ApiRepository.forPushPayload(
+ commits = commits.map{ commit => ApiCommit.forWebhookPayload(git, RepositoryName(repositoryInfo), commit) },
+ repository = ApiRepository.forWebhookPayload(
repositoryInfo,
owner= ApiUser(repositoryOwner))
)
+
+ def createDummyPayload(sender: Account): WebHookPushPayload =
+ WebHookPushPayload(
+ pusher = ApiPusher(sender),
+ sender = ApiUser(sender),
+ ref = "refs/heads/master",
+ before = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
+ after = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
+ commits = List.empty,
+ repository = ApiRepository.forDummyPayload(ApiUser(sender))
+ )
}
// https://developer.github.com/v3/activity/events/types/#issuesevent
@@ -367,6 +459,7 @@ object WebHookService {
def apply(action: String,
issue: Issue,
issueUser: Account,
+ assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -384,6 +477,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
+ assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -438,6 +532,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
+ assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -445,7 +540,7 @@ object WebHookService {
baseOwner: Account,
sender: Account,
mergedComment: Option[(IssueComment, Account)]
- ) : WebHookPullRequestReviewCommentPayload = {
+ ): WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
@@ -464,10 +559,60 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
+ assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,
sender = senderPayload)
}
}
+
+ // https://developer.github.com/v3/activity/events/types/#gollumevent
+ case class WebHookGollumPayload(
+ pages: Seq[WebHookGollumPagePayload],
+ repository: ApiRepository,
+ sender: ApiUser
+ ) extends WebHookPayload
+
+ case class WebHookGollumPagePayload(
+ page_name: String,
+ title: String,
+ summary: Option[String] = None,
+ action: String, // created or edited
+ sha: String, // SHA of the latest commit
+ html_url: ApiPath
+ )
+
+ object WebHookGollumPayload {
+ def apply(
+ action: String,
+ pageName: String,
+ sha: String,
+ repository: RepositoryInfo,
+ repositoryUser: Account,
+ sender: Account
+ ): WebHookGollumPayload = apply(Seq((action, pageName, sha)), repository, repositoryUser, sender)
+
+ def apply(
+ pages: Seq[(String, String, String)],
+ repository: RepositoryInfo,
+ repositoryUser: Account,
+ sender: Account
+ ): WebHookGollumPayload = {
+ WebHookGollumPayload(
+ pages = pages.map { case (action, pageName, sha) =>
+ WebHookGollumPagePayload(
+ action = action,
+ page_name = pageName,
+ title = pageName,
+ sha = sha,
+ html_url = ApiPath(s"/${RepositoryName(repository).fullName}/wiki/${StringUtil.urlDecode(pageName)}")
+ )
+ },
+ repository = ApiRepository(repository, repositoryUser),
+ sender = ApiUser(sender)
+ )
+ }
+ }
+
}
diff --git a/src/main/scala/gitbucket/core/service/WikiService.scala b/src/main/scala/gitbucket/core/service/WikiService.scala
index ab034fc1e..921db3fe2 100644
--- a/src/main/scala/gitbucket/core/service/WikiService.scala
+++ b/src/main/scala/gitbucket/core/service/WikiService.scala
@@ -75,22 +75,6 @@ trait WikiService {
}
}
- /**
- * Returns the content of the specified file.
- */
- def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] =
- using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
- if(!JGitUtil.isEmpty(git)){
- val index = path.lastIndexOf('/')
- val parentPath = if(index < 0) "." else path.substring(0, index)
- val fileName = if(index < 0) path else path.substring(index + 1)
-
- JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file =>
- git.getRepository.open(file.id).getBytes
- }
- } else None
- }
-
/**
* Returns the list of wiki page names.
*/
@@ -237,7 +221,7 @@ trait WikiService {
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
- if(message.trim.length == 0) {
+ if(message.trim.isEmpty) {
if(removed){
s"Rename ${currentPageName} to ${newPageName}"
} else if(created){
diff --git a/src/main/scala/gitbucket/core/servlet/CompositeScalatraFilter.scala b/src/main/scala/gitbucket/core/servlet/CompositeScalatraFilter.scala
new file mode 100644
index 000000000..beff25d50
--- /dev/null
+++ b/src/main/scala/gitbucket/core/servlet/CompositeScalatraFilter.scala
@@ -0,0 +1,73 @@
+package gitbucket.core.servlet
+
+import javax.servlet._
+import javax.servlet.http.HttpServletRequest
+
+import org.scalatra.ScalatraFilter
+
+import scala.collection.mutable.ListBuffer
+
+class CompositeScalatraFilter extends Filter {
+
+ private val filters = new ListBuffer[(ScalatraFilter, String)]()
+
+ def mount(filter: ScalatraFilter, path: String): Unit = {
+ filters += ((filter, path))
+ }
+
+ override def init(filterConfig: FilterConfig): Unit = {
+ filters.foreach { case (filter, _) =>
+ filter.init(filterConfig)
+ }
+ }
+
+ override def destroy(): Unit = {
+ filters.foreach { case (filter, _) =>
+ filter.destroy()
+ }
+ }
+
+ override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
+ val contextPath = request.getServletContext.getContextPath
+ val requestPath = request.asInstanceOf[HttpServletRequest].getRequestURI.substring(contextPath.length)
+ val checkPath = if(requestPath.endsWith("/")){
+ requestPath
+ } else {
+ requestPath + "/"
+ }
+
+ if(!checkPath.startsWith("/upload/") && !checkPath.startsWith("/git/") && !checkPath.startsWith("/git-lfs/") &&
+ !checkPath.startsWith("/plugin-assets/")){
+ filters
+ .filter { case (_, path) =>
+ val start = path.replaceFirst("/\\*$", "/")
+ checkPath.startsWith(start)
+ }
+ .foreach { case (filter, _) =>
+ val mockChain = new MockFilterChain()
+ filter.doFilter(request, response, mockChain)
+ if(mockChain.continue == false){
+ return ()
+ }
+ }
+ }
+
+
+ chain.doFilter(request, response)
+ }
+
+}
+
+class MockFilterChain extends FilterChain {
+ var continue: Boolean = false
+
+ override def doFilter(request: ServletRequest, response: ServletResponse): Unit = {
+ continue = true
+ }
+}
+
+//class FilterChainFilter(chain: FilterChain) extends Filter {
+// override def init(filterConfig: FilterConfig): Unit = ()
+// override def destroy(): Unit = ()
+// override def doFilter(request: ServletRequest, response: ServletResponse, mockChain: FilterChain) = chain.doFilter(request, response)
+//}
diff --git a/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala b/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala
deleted file mode 100644
index c712dd981..000000000
--- a/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package gitbucket.core.servlet
-
-import javax.servlet._
-import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
-
-import gitbucket.core.service.SystemSettingsService
-
-/**
- * A controller to provide GitHub compatible URL for Git clients.
- */
-class GHCompatRepositoryAccessFilter extends Filter with SystemSettingsService {
-
- /**
- * Pattern of GitHub compatible repository URL.
- * /:user/:repo.git/
- */
- private val githubRepositoryPattern = """^/[^/]+/[^/]+\.git/.*""".r
-
- override def init(filterConfig: FilterConfig) = {}
-
- override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) = {
- implicit val request = req.asInstanceOf[HttpServletRequest]
- val agent = request.getHeader("USER-AGENT")
- val response = res.asInstanceOf[HttpServletResponse]
- val requestPath = request.getRequestURI.substring(request.getContextPath.length)
-
- requestPath match {
- case githubRepositoryPattern() if agent != null && agent.toLowerCase.indexOf("git") >= 0 =>
- response.sendRedirect(baseUrl + "/git" + requestPath)
- case _ =>
- chain.doFilter(req, res)
- }
- }
-
- override def destroy() = {}
-
-}
diff --git a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala
index 7e0ba9797..d313661f0 100644
--- a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala
+++ b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala
@@ -51,21 +51,21 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
private def pluginRepository(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain,
settings: SystemSettings, isUpdating: Boolean, filter: GitRepositoryFilter): Unit = {
- implicit val r = request
+ Database() withSession { implicit session =>
+ val account = for {
+ auth <- Option(request.getHeader("Authorization"))
+ Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
+ account <- authenticate(settings, username, password)
+ } yield {
+ request.setAttribute(Keys.Request.UserName, account.userName)
+ account
+ }
- val account = for {
- auth <- Option(request.getHeader("Authorization"))
- Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
- account <- authenticate(settings, username, password)
- } yield {
- request.setAttribute(Keys.Request.UserName, account.userName)
- account
- }
-
- if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
- chain.doFilter(request, response)
- } else {
- AuthUtil.requireAuth(response)
+ if (filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)) {
+ chain.doFilter(request, response)
+ } else {
+ AuthUtil.requireAuth(response)
+ }
}
}
@@ -74,7 +74,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
val action = request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) =>
Database() withSession { implicit session =>
- getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
+ getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
case Some(repository) => {
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
// Authentication is not required
diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
index c4ffe413e..1583e76ac 100644
--- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
@@ -1,6 +1,7 @@
package gitbucket.core.servlet
import java.io.File
+import java.util
import java.util.Date
import gitbucket.core.api
@@ -22,6 +23,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.json4s.jackson.Serialization._
@@ -154,13 +156,23 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
+ val settings = loadSystemSettings()
+ val baseUrl = settings.baseUrl(request)
+ val sshUrl = settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" }
+
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
- val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
+ val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
}
+
+ if(repository.endsWith(".wiki")){
+ defining(request) { implicit r =>
+ receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl, sshUrl))
+ }
+ }
}
}
@@ -170,7 +182,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
-class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)/*(implicit session: Session)*/
+class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService with CommitsService {
@@ -185,9 +197,10 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
- .headOption.foreach { error =>
- command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
- }
+ .headOption
+ .foreach { error =>
+ command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
+ }
}
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
@@ -210,7 +223,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
- implicit val apiContext = api.JsonFormat.Context(baseUrl)
+ implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
@@ -285,12 +298,26 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call web hook
callWebHookOf(owner, repository, WebHook.Push) {
- for (pusherAccount <- getAccountByUserName(pusher);
- ownerAccount <- getAccountByUserName(owner)) yield {
+ for {
+ pusherAccount <- getAccountByUserName(pusher)
+ ownerAccount <- getAccountByUserName(owner)
+ } yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
}
}
+ if (command.getType == ReceiveCommand.Type.CREATE) {
+ callWebHookOf(owner, repository, WebHook.Create) {
+ for {
+ pusherAccount <- getAccountByUserName(pusher)
+ ownerAccount <- getAccountByUserName(owner)
+ } yield {
+ val refType = if (refName(1) == "tags") "tag" else "branch"
+ WebHookCreatePayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
+ ref = branchName, refType = refType)
+ }
+ }
+ }
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
@@ -309,6 +336,66 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
+class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
+ extends PostReceiveHook with WebHookService with AccountService with RepositoryService {
+
+ private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
+
+ override def onPostReceive(receivePack: ReceivePack, commands: util.Collection[ReceiveCommand]): Unit = {
+ Database() withTransaction { implicit session =>
+ try {
+ commands.asScala.headOption.foreach { command =>
+ implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
+ val refName = command.getRefName.split("/")
+ val commitIds = if (refName(1) == "tags") {
+ None
+ } else {
+ command.getType match {
+ case ReceiveCommand.Type.DELETE => None
+ case _ => Some((command.getOldId.getName, command.getNewId.name))
+ }
+ }
+
+ commitIds.map { case (oldCommitId, newCommitId) =>
+ val commits = using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
+ JGitUtil.getCommitLog(git, oldCommitId, newCommitId).flatMap { commit =>
+ val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
+ diffs.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
+ val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
+ val fileName = diff.newPath
+ (action, fileName, commit.id)
+ }
+ }
+ }
+
+ val pages = commits
+ .groupBy { case (action, fileName, commitId) => fileName }
+ .map { case (fileName, commits) =>
+ (commits.head._1, fileName, commits.last._3)
+ }
+
+ callWebHookOf(owner, repository, WebHook.Gollum) {
+ for {
+ pusherAccount <- getAccountByUserName(pusher)
+ repositoryUser <- getAccountByUserName(owner)
+ repositoryInfo <- getRepository(owner, repository)
+ } yield {
+ WebHookGollumPayload(pages.toSeq, repositoryInfo, repositoryUser, pusherAccount)
+ }
+ }
+ }
+ }
+ } catch {
+ case ex: Exception => {
+ logger.error(ex.toString, ex)
+ throw ex
+ }
+ }
+ }
+ }
+
+}
+
object GitLfs {
case class BatchRequest(
diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala
index 94dfc819e..f46dd827f 100644
--- a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala
+++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala
@@ -1,25 +1,30 @@
package gitbucket.core.servlet
-import java.io.File
+import java.io.{File, FileOutputStream}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule
-import gitbucket.core.plugin.PluginRegistry
+import gitbucket.core.plugin.{PluginRegistry, PluginRepository}
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
+import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.JDBCUtil._
import gitbucket.core.model.Profile.profile.blockingApi._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
-import javax.servlet.{ServletContextListener, ServletContextEvent}
-import org.apache.commons.io.FileUtils
+import javax.servlet.{ServletContextEvent, ServletContextListener}
+
+import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory
-import akka.actor.{Actor, Props, ActorSystem}
+import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
+import com.github.zafarkhaja.semver.{Version => Semver}
+
import scala.collection.JavaConverters._
+
/**
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
@@ -54,44 +59,11 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
val manager = new JDBCVersionManager(conn)
// Check version
- val versionFile = new File(GitBucketHome, "version")
-
- if(versionFile.exists()){
- val version = FileUtils.readFileToString(versionFile, "UTF-8")
- if(version == "3.14"){
- // Initialization for GitBucket 3.14
- logger.info("Migration to GitBucket 4.x start")
-
- // Backup current data
- val dataMvFile = new File(GitBucketHome, "data.mv.db")
- if(dataMvFile.exists) {
- FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
- }
- val dataTraceFile = new File(GitBucketHome, "data.trace.db")
- if(dataTraceFile.exists) {
- FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
- }
-
- // Change form
- manager.initialize()
- manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
- conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
- manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
- }
- conn.update("DROP TABLE PLUGIN")
- versionFile.delete()
-
- logger.info("Migration to GitBucket 4.x completed")
-
- } else {
- throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
- }
- }
+ checkVersion(manager, conn)
// Run normal migration
logger.info("Start schema update")
- val solidbase = new Solidbase()
- solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
+ new Solidbase().migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
@@ -106,6 +78,9 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
}
+ // Install bundled plugins
+ extractBundledPlugins(gitbucketVersion)
+
// Load plugins
logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
@@ -117,7 +92,76 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
+ private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
+ logger.info("Check version")
+ val versionFile = new File(GitBucketHome, "version")
+ if(versionFile.exists()){
+ val version = FileUtils.readFileToString(versionFile, "UTF-8")
+ if(version == "3.14"){
+ // Initialization for GitBucket 3.14
+ logger.info("Migration to GitBucket 4.x start")
+
+ // Backup current data
+ val dataMvFile = new File(GitBucketHome, "data.mv.db")
+ if(dataMvFile.exists) {
+ FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
+ }
+ val dataTraceFile = new File(GitBucketHome, "data.trace.db")
+ if(dataTraceFile.exists) {
+ FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
+ }
+
+ // Change form
+ manager.initialize()
+ manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
+ conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
+ manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
+ }
+ conn.update("DROP TABLE PLUGIN")
+ versionFile.delete()
+
+ logger.info("Migration to GitBucket 4.x completed")
+
+ } else {
+ throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
+ }
+ }
+ }
+
+ private def extractBundledPlugins(gitbucketVersion: String): Unit = {
+ logger.info("Extract bundled plugins")
+ val cl = Thread.currentThread.getContextClassLoader
+ try {
+ using(cl.getResourceAsStream("plugins/plugins.json")){ pluginsFile =>
+ if(pluginsFile != null){
+ val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
+
+ FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
+ FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
+
+ val plugins = PluginRepository.parsePluginJson(pluginsJson)
+ plugins.foreach { plugin =>
+ plugin.versions.sortBy { x => Semver.valueOf(x.version) }.reverse.zipWithIndex.foreach { case (version, i) =>
+ val file = new File(PluginRepository.LocalRepositoryDir, version.file)
+ if(!file.exists) {
+ logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
+ FileUtils.forceMkdirParent(file)
+ using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)){ case (in, out) => IOUtils.copy(in, out) }
+
+ if(plugin.default && i == 0){
+ logger.info(s"Enable ${file.getName} in default")
+ FileUtils.copyFile(file, new File(PluginHome, version.file))
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch {
+ case e: Exception => logger.error("Error in extracting bundled plugin", e)
+ }
+ }
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler
@@ -146,4 +190,4 @@ class DeleteOldActivityActor extends Actor with SystemSettingsService with Activ
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
index 6fe1ed34e..e2261a1e8 100644
--- a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
+++ b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala
@@ -19,7 +19,7 @@ class PluginAssetsServlet extends HttpServlet {
.find { case (prefix, _, _) => path.startsWith("/plugin-assets" + prefix) }
.flatMap { case (prefix, resourcePath, classLoader) =>
val resourceName = path.substring(("/plugin-assets" + prefix).length)
- Option(classLoader.getResourceAsStream(resourcePath.replaceFirst("^/", "") + resourceName))
+ Option(classLoader.getResourceAsStream(resourcePath.stripPrefix("/") + resourceName))
}
.map { in =>
try {
diff --git a/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala b/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala
new file mode 100644
index 000000000..1e9713f37
--- /dev/null
+++ b/src/main/scala/gitbucket/core/servlet/PluginControllerFilter.scala
@@ -0,0 +1,47 @@
+package gitbucket.core.servlet
+
+import javax.servlet._
+import javax.servlet.http.HttpServletRequest
+
+import gitbucket.core.controller.ControllerBase
+import gitbucket.core.plugin.PluginRegistry
+
+class PluginControllerFilter extends Filter {
+
+ private var filterConfig: FilterConfig = null
+
+ override def init(filterConfig: FilterConfig): Unit = {
+ this.filterConfig = filterConfig
+ }
+
+ override def destroy(): Unit = {
+ PluginRegistry().getControllers().foreach { case (controller, _) =>
+ controller.destroy()
+ }
+ }
+
+ override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
+ val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
+
+ PluginRegistry().getControllers()
+ .filter { case (_, path) =>
+ val start = path.replaceFirst("/\\*$", "/")
+ (requestUri + "/").startsWith(start)
+ }
+ .foreach { case (controller, _) =>
+ controller match {
+ case x: ControllerBase if(x.config == null) => x.init(filterConfig)
+ case _ => ()
+ }
+ val mockChain = new MockFilterChain()
+ controller.doFilter(request, response, mockChain)
+
+ if(mockChain.continue == false){
+ return ()
+ }
+ }
+
+ chain.doFilter(request, response)
+ }
+
+}
diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala
index 0629c14ce..59a0b1fd6 100644
--- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala
+++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala
@@ -19,7 +19,7 @@ import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand {
- val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
+ val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
}
@@ -154,7 +154,7 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
}
}
-class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
+class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String]) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService with DeployKeyService {
override protected def runTask(authType: AuthType): Unit = {
@@ -169,7 +169,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
val repository = git.getRepository
val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) {
- val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl)
+ val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
@@ -216,19 +216,26 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) exte
}
-class GitCommandFactory(baseUrl: String) extends CommandFactory {
+class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
import GitCommand._
logger.debug(s"command: $command")
- command match {
- case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
- case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
- case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
- case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
- case _ => new UnknownCommand(command)
+ val pluginCommand = PluginRegistry().getSshCommandProviders.collectFirst {
+ case f if f.isDefinedAt(command) => f(command)
+ }
+
+ pluginCommand match {
+ case Some(x) => x
+ case None => command match {
+ case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
+ case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
+ case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
+ case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
+ case _ => new UnknownCommand(command)
+ }
}
}
diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala
index 6d3c1238a..857db45cb 100644
--- a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala
+++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala
@@ -6,7 +6,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
-import gitbucket.core.util.{Directory}
+import gitbucket.core.util.Directory
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
@@ -22,7 +22,7 @@ object SshServer {
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
- server.setCommandFactory(new GitCommandFactory(baseUrl))
+ server.setCommandFactory(new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}")))
server.setShellFactory(new NoShell(sshAddress))
}
diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala
index 30e6ae0bc..806513020 100644
--- a/src/main/scala/gitbucket/core/util/Authenticator.scala
+++ b/src/main/scala/gitbucket/core/util/Authenticator.scala
@@ -97,16 +97,10 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with A
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map { repository =>
- if(!repository.repository.isPrivate){
+ if(isReadable(repository.repository, context.loginAccount)){
action(repository)
} else {
- context.loginAccount match {
- case Some(x) if(x.isAdmin) => action(repository)
- case Some(x) if(paths(0) == x.userName) => action(repository)
- case Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
- case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository)
- case _ => Unauthorized()
- }
+ Unauthorized()
}
} getOrElse NotFound()
}
diff --git a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala
index 644c3f25a..1ef20dcc7 100644
--- a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala
+++ b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala
@@ -4,11 +4,15 @@ import com.typesafe.config.ConfigFactory
import java.io.File
import Directory._
-import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingMySQLDriver, BlockingJdbcProfile}
+import ConfigUtil._
+import com.github.takezoe.slick.blocking.{BlockingH2Driver, BlockingJdbcProfile, BlockingMySQLDriver}
+import gitbucket.core.util.SyntaxSugars.defining
import liquibase.database.AbstractJdbcDatabase
import liquibase.database.core.{H2Database, MySQLDatabase, PostgresDatabase}
import org.apache.commons.io.FileUtils
+import scala.reflect.ClassTag
+
object DatabaseConfig {
private lazy val config = {
@@ -30,14 +34,14 @@ object DatabaseConfig {
ConfigFactory.parseFile(file)
}
- private lazy val dbUrl = config.getString("db.url")
+ private lazy val dbUrl = getValue("db.url", config.getString) //config.getString("db.url")
def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
lazy val url : String = url(None)
- lazy val user : String = config.getString("db.user")
- lazy val password : String = config.getString("db.password")
+ lazy val user : String = getValue("db.user", config.getString)
+ lazy val password : String = getValue("db.password", config.getString)
lazy val jdbcDriver : String = DatabaseType(url).jdbcDriver
lazy val slickDriver : BlockingJdbcProfile = DatabaseType(url).slickDriver
lazy val liquiDriver : AbstractJdbcDatabase = DatabaseType(url).liquiDriver
@@ -47,8 +51,16 @@ object DatabaseConfig {
lazy val minimumIdle : Option[Int] = getOptionValue("db.minimumIdle" , config.getInt)
lazy val maximumPoolSize : Option[Int] = getOptionValue("db.maximumPoolSize" , config.getInt)
+ private def getValue[T](path: String, f: String => T): T = {
+ getSystemProperty(path).getOrElse(getEnvironmentVariable(path).getOrElse{
+ f(path)
+ })
+ }
+
private def getOptionValue[T](path: String, f: String => T): Option[T] = {
- if(config.hasPath(path)) Some(f(path)) else None
+ getSystemProperty(path).orElse(getEnvironmentVariable(path).orElse {
+ if(config.hasPath(path)) Some(f(path)) else None
+ })
}
}
@@ -80,7 +92,7 @@ object DatabaseType {
}
object MySQL extends DatabaseType {
- val jdbcDriver = "com.mysql.jdbc.Driver"
+ val jdbcDriver = "org.mariadb.jdbc.Driver"
val slickDriver = BlockingMySQLDriver
val liquiDriver = new MySQLDatabase()
}
@@ -99,3 +111,33 @@ object DatabaseType {
}
}
}
+
+object ConfigUtil {
+
+ def getEnvironmentVariable[A](key: String): Option[A] = {
+ val value = System.getenv("GITBUCKET_" + key.toUpperCase.replace('.', '_'))
+ if(value != null && value.nonEmpty){
+ Some(convertType(value)).asInstanceOf[Option[A]]
+ } else {
+ None
+ }
+ }
+
+ def getSystemProperty[A](key: String): Option[A] = {
+ val value = System.getProperty("gitbucket." + key)
+ if(value != null && value.nonEmpty){
+ Some(convertType(value)).asInstanceOf[Option[A]]
+ } else {
+ None
+ }
+ }
+
+ def convertType[A: ClassTag](value: String) =
+ defining(implicitly[ClassTag[A]].runtimeClass){ c =>
+ if(c == classOf[Boolean]) value.toBoolean
+ else if(c == classOf[Long]) value.toLong
+ else if(c == classOf[Int]) value.toInt
+ else value
+ }
+
+}
diff --git a/src/main/scala/gitbucket/core/util/FileUtil.scala b/src/main/scala/gitbucket/core/util/FileUtil.scala
index 4f2a21d8e..070beef98 100644
--- a/src/main/scala/gitbucket/core/util/FileUtil.scala
+++ b/src/main/scala/gitbucket/core/util/FileUtil.scala
@@ -28,8 +28,6 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
- def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
-
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -53,24 +51,34 @@ object FileUtil {
}
}
- val mimeTypeWhiteList: Array[String] = Array(
- "application/pdf",
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- "image/gif",
- "image/jpeg",
- "image/png",
- "text/plain")
-
def getLfsFilePath(owner: String, repository: String, oid: String): String =
Directory.getLfsDir(owner, repository) + "/" + oid
def readableSize(size: Long): String = FileUtils.byteCountToDisplaySize(size)
+ /**
+ * Delete the given directory if it's empty.
+ * Do nothing if the given File is not a directory or not empty.
+ */
def deleteDirectoryIfEmpty(dir: File): Unit = {
if(dir.isDirectory() && dir.list().isEmpty) {
FileUtils.deleteDirectory(dir)
}
}
+
+ /**
+ * Delete file or directory forcibly.
+ */
+ def deleteIfExists(file: java.io.File): java.io.File = {
+ if(file.exists){
+ FileUtils.forceDelete(file)
+ }
+ file
+ }
+
+ lazy val MaxFileSize = if (System.getProperty("gitbucket.maxFileSize") != null)
+ System.getProperty("gitbucket.maxFileSize").toLong
+ else
+ 3 * 1024 * 1024
+
}
diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala
index 77767a3bc..393dbbf88 100644
--- a/src/main/scala/gitbucket/core/util/Implicits.scala
+++ b/src/main/scala/gitbucket/core/util/Implicits.scala
@@ -22,7 +22,8 @@ object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
- implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
+ implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
+ JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" })
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
@@ -77,11 +78,6 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/")
- def baseUrl:String = {
- val url = request.getRequestURL.toString
- val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
- url.substring(0, len).stripSuffix("/")
- }
}
implicit class RichSession(private val session: HttpSession) extends AnyVal {
diff --git a/src/main/scala/gitbucket/core/util/JDBCUtil.scala b/src/main/scala/gitbucket/core/util/JDBCUtil.scala
index ffb39eff5..8a8574c3d 100644
--- a/src/main/scala/gitbucket/core/util/JDBCUtil.scala
+++ b/src/main/scala/gitbucket/core/util/JDBCUtil.scala
@@ -75,7 +75,7 @@ object JDBCUtil {
var stringLiteral = false
while({ length = in.read(bytes); length != -1 }){
- for(i <- 0 to length - 1){
+ for(i <- 0 until length){
val c = bytes(i)
if(c == '\''){
stringLiteral = !stringLiteral
@@ -146,13 +146,11 @@ object JDBCUtil {
}
}
- val columnValues = values.map { value =>
- value match {
- case x: String => "'" + x.replace("'", "''") + "'"
- case x: Timestamp => "'" + dateFormat.format(x) + "'"
- case null => "NULL"
- case x => x
- }
+ val columnValues = values.map {
+ case x: String => "'" + x.replace("'", "''") + "'"
+ case x: Timestamp => "'" + dateFormat.format(x) + "'"
+ case null => "NULL"
+ case x => x
}
sb.append(columnValues.mkString(", "))
sb.append(");\n")
diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala
index 3c494aed4..c5fe1cd3d 100644
--- a/src/main/scala/gitbucket/core/util/JGitUtil.scala
+++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala
@@ -1,5 +1,7 @@
package gitbucket.core.util
+import java.io.ByteArrayOutputStream
+
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
@@ -14,15 +16,17 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
-import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
+import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
-import org.cache2k.{Cache2kBuilder, CacheEntry}
+import org.cache2k.Cache2kBuilder
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
+import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter, RawTextComparator}
import org.eclipse.jgit.dircache.DirCacheEntry
+import org.eclipse.jgit.util.io.DisabledOutputStream
import org.slf4j.LoggerFactory
/**
@@ -93,7 +97,7 @@ object JGitUtil {
val summary = getSummaryMessage(fullMessage, shortMessage)
- val description = defining(fullMessage.trim.indexOf("\n")){ i =>
+ val description = defining(fullMessage.trim.indexOf('\n')){ i =>
if(i >= 0){
Some(fullMessage.trim.substring(i).trim)
} else None
@@ -114,7 +118,8 @@ object JGitUtil {
newObjectId: Option[String],
oldMode: String,
newMode: String,
- tooLarge: Boolean
+ tooLarge: Boolean,
+ patch: Option[String]
)
/**
@@ -146,9 +151,10 @@ object JGitUtil {
*
* @param name the module name
* @param path the path in the repository
- * @param url the repository url of this module
+ * @param repositoryUrl the repository url of this module
+ * @param viewerUrl the repository viewer url of this module
*/
- case class SubmoduleInfo(name: String, path: String, url: String)
+ case class SubmoduleInfo(name: String, path: String, repositoryUrl: String, viewerUrl: String)
case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean)
@@ -184,11 +190,9 @@ object JGitUtil {
val dir = git.getRepository.getDirectory
val keyPrefix = dir.getAbsolutePath + "@"
- cache.forEach(new Consumer[CacheEntry[String, Int]] {
- override def accept(entry: CacheEntry[String, Int]): Unit = {
- if(entry.getKey.startsWith(keyPrefix)){
- cache.remove(entry.getKey)
- }
+ cache.keys.forEach(key => {
+ if (key.startsWith(keyPrefix)) {
+ cache.remove(key)
}
})
}
@@ -226,9 +230,14 @@ object JGitUtil {
ref.getName.stripPrefix("refs/heads/")
}.toList,
// tags
- git.tagList.call.asScala.map { ref =>
- val revCommit = getRevCommitFromId(git, ref.getObjectId)
- TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
+ git.tagList.call.asScala.flatMap { ref =>
+ try {
+ val revCommit = getRevCommitFromId(git, ref.getObjectId)
+ Some(TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName))
+ } catch {
+ case _: IncorrectObjectTypeException =>
+ None
+ }
}.sortBy(_.time).toList
)
} catch {
@@ -244,9 +253,10 @@ object JGitUtil {
* @param git the Git object
* @param revision the branch name or commit id
* @param path the directory path (optional)
+ * @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
* @return HTML of the file list
*/
- def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
+ def getFileList(git: Git, revision: String, path: String = ".", baseUrl: Option[String] = None): List[FileInfo] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId == null) return Nil
@@ -288,7 +298,7 @@ object JGitUtil {
@tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, RevCommit])],
- revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] ={
+ revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] = {
if(restList.isEmpty){
result
} else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
@@ -332,7 +342,7 @@ object JGitUtil {
useTreeWalk(revCommit){ treeWalk =>
while (treeWalk.next()) {
val linkUrl = if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
- getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
+ getSubmodules(git, revCommit.getTree, baseUrl).find(_.path == treeWalk.getPathString).map(_.viewerUrl)
} else None
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, treeWalk.getPathString, linkUrl)
}
@@ -359,9 +369,9 @@ object JGitUtil {
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
- case _ => file1.name.compareTo(file2.name) < 0
+ case _ => file1.name.compareTo(file2.name) < 0
}
- }.toList
+ }
}
}
@@ -369,7 +379,7 @@ object JGitUtil {
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
- defining(fullMessage.trim.indexOf("\n")){ i =>
+ defining(fullMessage.trim.indexOf('\n')){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
@@ -509,90 +519,49 @@ object JGitUtil {
}.toMap
}
- /**
- * Returns the tuple of diff of the given commit and the previous commit id.
- */
- def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
- @scala.annotation.tailrec
- def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
- i.hasNext match {
- case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next)
- case _ => logs
- }
+ def getPatch(git: Git, from: Option[String], to: String): String = {
+ val out = new ByteArrayOutputStream()
+ val df = new DiffFormatter(out)
+ df.setRepository(git.getRepository)
+ df.setDiffComparator(RawTextComparator.DEFAULT)
+ df.setDetectRenames(true)
+ df.format(getDiffEntries(git, from, to).head)
+ new String(out.toByteArray, "UTF-8")
+ }
+ private def getDiffEntries(git: Git, from: Option[String], to: String): Seq[DiffEntry] = {
using(new RevWalk(git.getRepository)){ revWalk =>
- revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id)))
- val commits = getCommitLog(revWalk.iterator, Nil)
- val revCommit = commits(0)
+ val df = new DiffFormatter(DisabledOutputStream.INSTANCE)
+ df.setRepository(git.getRepository)
- if(commits.length >= 2){
- // not initial commit
- val oldCommit = if(revCommit.getParentCount >= 2) {
- // merge commit
- revCommit.getParents.head
- } else {
- commits(1)
- }
- (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
-
- } else {
- // initial commit
- using(new TreeWalk(git.getRepository)){ treeWalk =>
- treeWalk.setRecursive(true)
- treeWalk.addTree(revCommit.getTree)
- val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
- while(treeWalk.next){
- val newIsImage = FileUtil.isImage(treeWalk.getPathString)
- buffer.append((if(!fetchContent){
- DiffInfo(
- changeType = ChangeType.ADD,
- oldPath = null,
- newPath = treeWalk.getPathString,
- oldContent = None,
- newContent = None,
- oldIsImage = false,
- newIsImage = newIsImage,
- oldObjectId = None,
- newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
- oldMode = treeWalk.getFileMode(0).toString,
- newMode = treeWalk.getFileMode(0).toString,
- tooLarge = false
- )
- } else {
- DiffInfo(
- changeType = ChangeType.ADD,
- oldPath = null,
- newPath = treeWalk.getPathString,
- oldContent = None,
- newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
- oldIsImage = false,
- newIsImage = newIsImage,
- oldObjectId = None,
- newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
- oldMode = treeWalk.getFileMode(0).toString,
- newMode = treeWalk.getFileMode(0).toString,
- tooLarge = false
- )
- }))
+ val toCommit = revWalk.parseCommit(git.getRepository.resolve(to))
+ from match {
+ case None => {
+ toCommit.getParentCount match {
+ case 0 => df.scan(new EmptyTreeIterator(), new CanonicalTreeParser(null, git.getRepository.newObjectReader(), toCommit.getTree)).asScala
+ case _ => df.scan(toCommit.getParent(0), toCommit.getTree).asScala
}
- (buffer.toList, None)
+ }
+ case Some(from) => {
+ val fromCommit = revWalk.parseCommit(git.getRepository.resolve(from))
+ df.scan(fromCommit.getTree, toCommit.getTree).asScala
}
}
}
}
- 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}"))
+ def getParentCommitId(git: Git, id: String): Option[String] = {
+ using(new RevWalk(git.getRepository)){ revWalk =>
+ val commit = revWalk.parseCommit(git.getRepository.resolve(id))
+ commit.getParentCount match {
+ case 0 => None
+ case _ => Some(commit.getParent(0).getName)
+ }
+ }
+ }
- val newTreeIter = new CanonicalTreeParser
- newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
-
- import scala.collection.JavaConverters._
- git.getRepository.getConfig.setString("diff", null, "renames", "copies")
-
- val diffs = git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala
+ def getDiffs(git: Git, from: Option[String], to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
+ val diffs = getDiffEntries(git, from, to)
diffs.map { diff =>
if(diffs.size > 100){
DiffInfo(
@@ -607,7 +576,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
- tooLarge = true
+ tooLarge = true,
+ patch = None
)
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
@@ -625,7 +595,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
- tooLarge = false
+ tooLarge = false,
+ patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
} else {
DiffInfo(
@@ -640,13 +611,23 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
- tooLarge = false
+ tooLarge = false,
+ patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None) // TODO use DiffFormatter
)
}
}
}.toList
}
+ private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
+ val out = new ByteArrayOutputStream()
+ using(new DiffFormatter(out)){ formatter =>
+ formatter.setRepository(git.getRepository)
+ formatter.format(diff)
+ val patch = new String(out.toByteArray) // TODO charset???
+ patch.split("\n").drop(4).mkString("\n")
+ }
+ }
/**
* Returns the list of branch names of the specified commit.
@@ -751,7 +732,7 @@ object JGitUtil {
/**
* Read submodule information from .gitmodules
*/
- def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = {
+ def getSubmodules(git: Git, tree: RevTree, baseUrl: Option[String]): List[SubmoduleInfo] = {
val repository = git.getRepository
getContentFromPath(git, tree, ".gitmodules", true).map { bytes =>
(try {
@@ -759,7 +740,7 @@ object JGitUtil {
config.getSubsections("submodule").asScala.map { module =>
val path = config.getString("submodule", module, "path")
val url = config.getString("submodule", module, "url")
- SubmoduleInfo(module, path, url)
+ SubmoduleInfo(module, path, url, StringUtil.getRepositoryViewerUrl(url, baseUrl))
}
} catch {
case e: ConfigInvalidException => {
@@ -823,17 +804,22 @@ object JGitUtil {
}
}
+ def isLfsPointer(loader: ObjectLoader): Boolean = {
+ !loader.isLarge && new String(loader.getBytes(), "UTF-8").startsWith("version https://git-lfs.github.com/spec/v1")
+ }
+
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(objectId)
+ val isLfs = isLfsPointer(loader)
val large = FileUtil.isLarge(loader.getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
val size = Some(getContentSize(loader))
if(viewer == "other"){
- if(bytes.isDefined && FileUtil.isText(bytes.get)){
+ if(!isLfs && bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", size, Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
@@ -994,13 +980,13 @@ object JGitUtil {
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId =>
- val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
+ val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository)
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
- val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
+ val commits = 0.to(blame.getResultContents().size() - 1).map { i =>
val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo(
@@ -1010,7 +996,7 @@ object JGitUtil {
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
- if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
+ if(blame.getSourcePath(i)==path){ None } else { Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty)
diff --git a/src/main/scala/gitbucket/core/util/Mailer.scala b/src/main/scala/gitbucket/core/util/Mailer.scala
new file mode 100644
index 000000000..703f4b488
--- /dev/null
+++ b/src/main/scala/gitbucket/core/util/Mailer.scala
@@ -0,0 +1,68 @@
+package gitbucket.core.util
+
+import gitbucket.core.model.Account
+import gitbucket.core.service.SystemSettingsService
+
+import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
+import SystemSettingsService.SystemSettings
+
+class Mailer(settings: SystemSettings){
+
+ def send(to: String, subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
+ createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
+ email.addTo(to).send
+ }
+ }
+
+ def sendBcc(bcc: Seq[String], subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
+ createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
+ bcc.foreach { address =>
+ email.addBcc(address)
+ }
+ email.send()
+ }
+ }
+
+ def createMail(subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Option[HtmlEmail] = {
+ if(settings.notification == true){
+ settings.smtp.map { smtp =>
+ val email = new HtmlEmail
+ email.setHostName(smtp.host)
+ email.setSmtpPort(smtp.port.get)
+ smtp.user.foreach { user =>
+ email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
+ }
+ smtp.ssl.foreach { ssl =>
+ email.setSSLOnConnect(ssl)
+ if(ssl == true) {
+ email.setSslSmtpPort(smtp.port.get.toString)
+ }
+ }
+ smtp.starttls.foreach { starttls =>
+ email.setStartTLSEnabled(starttls)
+ email.setStartTLSRequired(starttls)
+ }
+ smtp.fromAddress
+ .map (_ -> smtp.fromName.getOrElse(loginAccount.map(_.userName).getOrElse("GitBucket")))
+ .orElse (Some("notifications@gitbucket.com" -> loginAccount.map(_.userName).getOrElse("GitBucket")))
+ .foreach { case (address, name) =>
+ email.setFrom(address, name)
+ }
+ email.setCharset("UTF-8")
+ email.setSubject(subject)
+ email.setTextMsg(textMsg)
+ htmlMsg.foreach { msg =>
+ email.setHtmlMsg(msg)
+ }
+
+ email
+ }
+ } else None
+ }
+
+}
+
+//class MockMailer extends Notifier {
+// def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
+// (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
+//}
diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala
deleted file mode 100644
index 3c8dba558..000000000
--- a/src/main/scala/gitbucket/core/util/Notifier.scala
+++ /dev/null
@@ -1,142 +0,0 @@
-package gitbucket.core.util
-
-import gitbucket.core.model.{Session, Issue, Account}
-import gitbucket.core.model.Profile.profile.blockingApi._
-import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
-import gitbucket.core.servlet.Database
-import gitbucket.core.view.Markdown
-
-import scala.concurrent._
-import scala.util.{Success, Failure}
-import ExecutionContext.Implicits.global
-import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
-import org.slf4j.LoggerFactory
-import gitbucket.core.controller.Context
-import SystemSettingsService.Smtp
-import SyntaxSugars.defining
-
-trait Notifier extends RepositoryService with AccountService with IssuesService {
-
- def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
- (msg: String => String)(implicit context: Context): Unit
-
- protected def recipients(issue: Issue, loginAccount: Account)(notify: String => Unit)(implicit session: Session) =
- (
- // individual repository's owner
- issue.userName ::
- // group members of group repository
- getGroupMembers(issue.userName).map(_.userName) :::
- // collaborators
- getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
- // participants
- issue.openedUserName ::
- getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
- )
- .distinct
- .withFilter ( _ != loginAccount.userName ) // the operation in person is excluded
- .foreach (
- getAccountByUserName(_)
- .filterNot (_.isGroupAccount)
- .filterNot (LDAPUtil.isDummyMailAddress(_))
- .foreach (x => notify(x.mailAddress))
- )
-}
-
-object Notifier {
- // TODO We want to be able to switch to mock.
- def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
- case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
- case _ => new MockMailer
- }
-
- def msgIssue(url: String) = (content: String) => s"""
- |${content}+ Webhooks allow external services to be notified when certain events happen within your repository. + When the specified events happen, we’ll send a POST request to each of the URLs you provide. + Learn more in GitBucket Wiki Webhook Page. +
+ Add webhook + +| + + @webHook.url + + (@events.map(_.name).mkString(", ")) + | + + |
@description
+@description
} @if(account.url.isDefined){ } -+
Joined on @helpers.date(account.registeredDate)