Compare commits

..

3 Commits

Author SHA1 Message Date
Naoki Takezoe
47ec2d4712 Copy only LFS dir and insert default priorities in forking repository 2017-07-05 00:21:15 +09:00
Naoki Takezoe
31160f42cb Release 4.14.1 2017-07-05 00:01:33 +09:00
Naoki Takezoe
4f11cbaa77 (refs #1631)Copy repository files directory only if it exists 2017-07-04 23:57:18 +09:00
310 changed files with 12753 additions and 13715 deletions

View File

@@ -1,6 +1,7 @@
# The guidelines for contributing # Guideline for Issues
- 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. - At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or 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. - If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- 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. - We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- 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. - 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.

View File

@@ -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) - [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] searched for similar already existing issue - [] searched for similar already existing issue
@@ -9,10 +9,11 @@
## Issue ## Issue
**Impacted version**: xxxx **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**: **Problem description**:
- *be as explicit has you can* - *be as explicit has you can*
- *describe the problem and its symptoms* - *describe the problem and its symptoms*
- *explain how to reproduce* - *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)* - *attach whatever information that can help understanding the context (screen capture, log files)*
- *do your best to use a correct english (re-read yourself)*

View File

@@ -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) - [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] rebased my branch over master - [] rebased my branch over master

5
.github/SUPPORT.md vendored
View File

@@ -1,5 +0,0 @@
# 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.

View File

@@ -1,7 +1,5 @@
language: scala language: scala
sudo: true sudo: true
jdk:
- oraclejdk8
script: script:
- sbt test - sbt test
before_script: before_script:
@@ -16,3 +14,33 @@ cache:
- $HOME/.coursier - $HOME/.coursier
- $HOME/.embedmysql - $HOME/.embedmysql
- $HOME/.embedpostgresql - $HOME/.embedpostgresql
matrix:
include:
- jdk: oraclejdk8
addons:
apt:
packages:
- libaio1
- dist: trusty
group: edge
sudo: required
jdk: oraclejdk9
addons:
apt:
packages:
- libaio1
- oracle-java9-installer
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
- java -version
- 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 ..
- 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

View File

@@ -1,432 +0,0 @@
# Changelog
All changes to the project will be documented in this file.
## 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

408
README.md
View File

@@ -57,35 +57,413 @@ GitBucket has a plug-in system that allows extra functionality. Officially the f
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) - [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin) - [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-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](https://gitbucket-plugins.github.io/). You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/).
Support 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 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. - 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. - 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.
What's New in 4.19.x Release Notes
------------- -------------
### 4.14.1 - 4 Jul 2017
- Bug fix: Possibility of error in forking repository
### 4.19.2 - 3 Dec 2017 ### 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
- Fix routing bug in `CompositeScalatraFilter` ### 4.13 - 29 May 2017
- Resolve id attribute collision in the web hook editing form - Uploading files into the repository
- HTML is available in Markdown
- Added filter box to dropdown menus
### 4.19.1 - 2 Dec 2017 ### 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
- Update gitbucket-notifications-plugin because it had a version compatibility issue ### 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.19.0 - 2 Dec 2017 ### 4.10 - 25 Feb 2017
- Update to Scala 2.12, Scalatra 2.5 and Slick 3.2
- Display file size in the file viewer
- [gitbucket-maven-repository-plugin](https://github.com/takezoe/gitbucket-maven-repository-plugin) is available ### 4.9 - 29 Jan 2017
- Upgrade to Scalatra 2.6 - GitLFS support
- Improve layout of the system settings page - Template for issues and pull requests
- New extension point (`sshCommandProvider`) - Manual label color editing
- 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. - 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)
See the [change log](CHANGELOG.md) for all of the updates. ### 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

View File

@@ -1,21 +1,16 @@
import com.typesafe.sbt.license.{LicenseInfo, DepModuleInfo}
import com.typesafe.sbt.pgp.PgpKeys._
val Organization = "io.github.gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.19.2" val GitBucketVersion = "4.14.1"
val ScalatraVersion = "2.6.1" val ScalatraVersion = "2.5.0"
val JettyVersion = "9.4.7.v20170914" val JettyVersion = "9.3.19.v20170502"
lazy val root = (project in file(".")).enablePlugins(SbtTwirl, ScalatraPlugin, JRebelPlugin).settings( lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
)
sourcesInBase := false sourcesInBase := false
organization := Organization organization := Organization
name := Name name := Name
version := GitBucketVersion version := GitBucketVersion
scalaVersion := "2.12.4" scalaVersion := "2.12.2"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
@@ -26,24 +21,25 @@ resolvers ++= Seq(
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/" "amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
) )
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.9.0.201710071750-r", "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.7.0.201704051617-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.9.0.201710071750-r", "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.7.0.201704051617-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.scalatra" %% "scalatra-forms" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.5.1", "org.json4s" %% "json4s-jackson" % "3.5.1",
"io.github.gitbucket" %% "scalatra-forms" % "1.1.0",
"commons-io" % "commons-io" % "2.5", "commons-io" % "commons-io" % "2.5",
"io.github.gitbucket" % "solidbase" % "1.0.2", "io.github.gitbucket" % "solidbase" % "1.0.2",
"io.github.gitbucket" % "markedj" % "1.0.15", "io.github.gitbucket" % "markedj" % "1.0.12",
"org.apache.commons" % "commons-compress" % "1.13", "org.apache.commons" % "commons-compress" % "1.13",
"org.apache.commons" % "commons-email" % "1.4", "org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.3", "org.apache.httpcomponents" % "httpclient" % "4.5.3",
"org.apache.sshd" % "apache-sshd" % "1.4.0" exclude("org.slf4j","slf4j-jdk14"), "org.apache.sshd" % "apache-sshd" % "1.4.0" exclude("org.slf4j","slf4j-jdk14"),
"org.apache.tika" % "tika-core" % "1.14", "org.apache.tika" % "tika-core" % "1.14",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.10", "com.github.takezoe" %% "blocking-slick-32" % "0.0.8",
"joda-time" % "joda-time" % "2.9.9",
"com.novell.ldap" % "jldap" % "2009-10-07", "com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.195", "com.h2database" % "h2" % "1.4.195",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.1.2", "mysql" % "mysql-connector-java" % "6.0.6",
"org.postgresql" % "postgresql" % "42.0.0", "org.postgresql" % "postgresql" % "42.0.0",
"ch.qos.logback" % "logback-classic" % "1.2.3", "ch.qos.logback" % "logback-classic" % "1.2.3",
"com.zaxxer" % "HikariCP" % "2.6.1", "com.zaxxer" % "HikariCP" % "2.6.1",
@@ -54,15 +50,13 @@ libraryDependencies ++= Seq(
"org.cache2k" % "cache2k-all" % "1.0.0.CR1", "org.cache2k" % "cache2k-all" % "1.0.0.CR1",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"), "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
"net.coobird" % "thumbnailator" % "0.4.8", "net.coobird" % "thumbnailator" % "0.4.8",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test", "junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.mockito" % "mockito-core" % "2.7.22" % "test", "org.mockito" % "mockito-core" % "2.7.22" % "test",
"com.wix" % "wix-embedded-mysql" % "2.1.4" % "test", "com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test"
"net.i2p.crypto" % "eddsa" % "0.1.0"
) )
// Compiler settings // Compiler settings
@@ -91,22 +85,17 @@ assemblyMergeStrategy in assembly := {
} }
// JRebel // JRebel
//Seq(jrebelSettings: _*) Seq(jrebelSettings: _*)
//jrebel.webLinks += (target in webappPrepare).value jrebel.webLinks += (target in webappPrepare).value
//jrebel.enabled := System.getenv().get("JREBEL") != null jrebel.enabled := System.getenv().get("JREBEL") != null
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path => 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 // Create executable war file
val ExecutableConfig = config("executable").hide val executableConfig = config("executable").hide
Keys.ivyConfigurations += ExecutableConfig Keys.ivyConfigurations += executableConfig
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
@@ -135,7 +124,7 @@ executableKey := {
IO delete temp IO delete temp
// include jetty classes // 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 => jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) => IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") || (name startsWith "javax/") ||
@@ -154,26 +143,14 @@ executableKey := {
IO copyFile (classDir / name, temp / name) 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.parse(json).foreach { case (plugin, version, file) =>
val url = s"https://github.com/gitbucket/${plugin}/releases/download/${version}/${file}"
log info s"Download: ${url}"
IO transfer(new java.net.URL(url).openStream, pluginsDir / file)
}
// zip it up // zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF") IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.allPaths --- PathFinder(temp)).get pair { file => IO.relativizeFile(temp, file) } val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
val manifest = new JarManifest val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName val outputFile = workDir / warName
IO jar (contentMappings.map { case (file, path) => (file, path.toString) } , outputFile, manifest) IO jar (contentMappings, outputFile, manifest)
// generate checksums // generate checksums
Seq( Seq(
@@ -193,7 +170,7 @@ executableKey := {
publishTo := { publishTo := {
val nexus = "https://oss.sonatype.org/" val nexus = "https://oss.sonatype.org/"
if (version.value.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") 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 publishMavenStyle := true
pomIncludeRepository := { _ => false } pomIncludeRepository := { _ => false }
@@ -242,8 +219,3 @@ pomExtra := (
</developer> </developer>
</developers> </developers>
) )
licenseOverrides := {
case DepModuleInfo("com.github.bkromhout", "java-diff-utils", _) =>
LicenseInfo(LicenseCategory.Apache, "Apache-2.0", "http://www.apache.org/licenses/LICENSE-2.0")
}

View File

@@ -1,25 +1,35 @@
JRebel integration (optional) JRebel integration (optional)
============================= =============================
[JRebel](https://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster. [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 slow "app restart" per modification of codes. Alsp it's only used during development, and doesn't change your deployed app in any way. JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
JRebel is not open source, but we can use it free for non-commercial use. ```
> 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.
---- ----
## 1. Get a JRebel license ## 1. Get a JRebel license
Sign up for a [myJRebel](https://my.jrebel.com/register). You will need to create an account. Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
## 2. Download JRebel ## 2. Download JRebel
Download the most recent ["nosetup" JRebel zip](https://zeroturnaround.com/software/jrebel/download/prev-releases/). Download the most recent ["nosetup" JRebel zip](http://zeroturnaround.com/software/jrebel/download/prev-releases/).
Next, unzip the downloaded file. Next, unzip the downloaded file.
## 3. Activate ## 3. Activate
Follow `readme.txt` in the extracted directory to activate your downloaded JRebel. 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.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment. You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
@@ -31,13 +41,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: To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash ```bash
export JREBEL=/path/to/jrebel/legacy/jrebel.jar export JREBEL=/path/to/jrebel/jrebel.jar
``` ```
For example, if you unzipped your JRebel download in your home directory, you whould use: For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash ```bash
export JREBEL=~/jrebel/legacy/jrebel.jar export JREBEL=~/jrebel/jrebel.jar
``` ```
Now reload your shell: Now reload your shell:
@@ -63,26 +73,39 @@ $ ./sbt
You will start the servlet container slightly differently now that you're using sbt. You will start the servlet container slightly differently now that you're using sbt.
``` ```
> jetty:quickstart > jetty:start
: :
2017-09-21 15:46:35 JRebel: [info] starting server ...
2017-09-21 15:46:35 JRebel: ############################################################# [success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
2017-09-21 15:46:35 JRebel: 2016-01-03 21:47:57 JRebel:
2017-09-21 15:46:35 JRebel: Legacy Agent 7.0.15 (201709080836) 2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
2017-09-21 15:46:35 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu. 2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
2017-09-21 15:46:35 JRebel: 2016-01-03 21:47:57 JRebel:
2017-09-21 15:46:35 JRebel: Over the last 2 days JRebel prevented 2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
2017-09-21 15:46:35 JRebel: at least 8 redeploys/restarts saving you about 0.3 hours. 2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
2017-09-21 15:46:35 JRebel: 2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
2017-09-21 15:46:35 JRebel: Licensed to Naoki Takezoe (using myJRebel). 2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
2017-09-21 15:46:35 JRebel: 2016-01-03 21:48:00 JRebel:
2017-09-21 15:46:35 JRebel: 2016-01-03 21:48:00 JRebel: #############################################################
2017-09-21 15:46:35 JRebel: ############################################################# 2016-01-03 21:48:00 JRebel:
2017-09-21 15:46:35 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:
: :
> ~compile > ~ copy-resources
[success] Total time: 2 s, completed 2017/09/21 15:50:06 [success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
1. Waiting for source changes... (press enter to interrupt) 1. Waiting for source changes... (press enter to interrupt)
``` ```
@@ -91,11 +114,12 @@ For example, you can change the title on `src/main/twirl/gitbucket/core/main.sca
```html ```html
: :
<a href="@context.path/" class="logo"> <a class="navbar-brand" href="@path/">
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/> <img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
GitBucket @defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
change code !!!!!!!!!!!!!!!! change code !!!!!!!!!!!!!!!!
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a> </a>
: :
``` ```
@@ -104,17 +128,21 @@ If JRebel is doing is correctly installed you will see a notice for you:
``` ```
1. Waiting for source changes... (press enter to interrupt) 1. Waiting for source changes... (press enter to interrupt)
[info] Compiling 1 Scala source to /Users/naoki.takezoe/gitbucket/target/scala-2.12/classes... 2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
[success] Total time: 1 s, completed 2017/09/21 15:55:40 [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)
``` ```
And you reload browser, JRebel give notice of that it has reloaded classes: 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) 2. Waiting for source changes... (press enter to interrupt)
2017-09-21 15:55:40 JRebel: Reloading class 'gitbucket.core.html.main$'. 2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
``` ```
## 6. Limitations ## 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 routing patterns, there is nothing JRebel can do, you will have to restart by `jetty:quickstart`. 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`.

View File

@@ -1,101 +0,0 @@
# 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 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.googlecode.javaewah # JavaEWAH # 1.1.6 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0) | org.cache2k # cache2k-all # 1.0.0.CR1 | <notextile></notextile>
Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.objenesis # objenesis # 2.5 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # apache-sshd # 1.4.0 | <notextile></notextile>
Apache | [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) | org.apache.sshd # sshd-core # 1.4.0 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.typesafe # config # 1.3.1 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.typesafe.akka # akka-actor_2.12 # 2.5.0 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-io # commons-io # 2.5 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | fr.brouillard.oss.security.xhub # xhub4j-core # 1.0.0 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-compress # 1.13 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-email # 1.4 | <notextile></notextile>
Apache | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.commons # commons-lang3 # 3.5 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpclient # 4.5.3 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpcore # 4.4.6 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.httpcomponents # httpmime # 4.5.2 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.apache.tika # tika-core # 1.14 | <notextile></notextile>
Apache | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.liquibase # liquibase-core # 3.4.1 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-http # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-io # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-security # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-server # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-servlet # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-util # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-webapp # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License - Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.eclipse.jetty # jetty-xml # 9.2.19.v20160908 | <notextile></notextile>
Apache | [Apache Software License, Version 1.1](http://www.apache.org/licenses/LICENSE-1.1) | org.bouncycastle # bcpg-jdk15on # 1.56 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.github.bkromhout # java-diff-utils # 2.1.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.html) | com.typesafe.play # twirl-api_2.12 # 1.3.7 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-ast_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-core_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-jackson_2.12 # 3.5.1 | <notextile></notextile>
Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.json4s # json4s-scalap_2.12 # 3.5.1 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.enragedginger # akka-quartz-scheduler_2.12 # 1.6.0-akka-2.4.x | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-annotations # 2.8.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-core # 2.8.4 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.fasterxml.jackson.core # jackson-databind # 2.8.4 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.github.takezoe # blocking-slick-32_2.12 # 0.0.10 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.google.code.findbugs # jsr305 # 3.0.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.zaxxer # HikariCP # 2.6.1 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-codec # commons-codec # 1.9 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | commons-logging # commons-logging # 1.2 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | de.flapdoodle.embed # de.flapdoodle.embed.process # 2.0.1 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | eu.medsea.mimeutil # mime-util # 2.1.3 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # markedj # 1.0.15 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # scalatra-forms_2.12 # 1.1.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | io.github.gitbucket # solidbase # 1.0.2 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy # 1.6.11 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.bytebuddy # byte-buddy-agent # 1.6.11 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | org.quartz-scheduler # quartz # 2.2.3 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | ru.yandex.qatools.embed # postgresql-embedded # 2.0 | <notextile></notextile>
Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | tomcat # tomcat-apr # 5.5.23 | <notextile></notextile>
Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalactic # scalactic_2.12 # 3.0.0 | <notextile></notextile>
Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest_2.12 # 3.0.0 | <notextile></notextile>
BSD | [BSD](LICENSE.txt) | com.thoughtworks.paranamer # paranamer # 2.8 | <notextile></notextile>
BSD | [BSD](http://software.clapper.org/grizzled-slf4j/license.html) | org.clapper # grizzled-slf4j_2.12 # 1.3.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-common_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-json_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-scalatest_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra-test_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD](http://github.com/scalatra/scalatra/raw/HEAD/LICENSE) | org.scalatra # scalatra_2.12 # 2.5.0 | <notextile></notextile>
BSD | [BSD 3-Clause](http://www.scala-lang.org/license.html) | org.scala-lang # scala-library # 2.12.3 | <notextile></notextile>
BSD | [BSD 3-Clause](http://www.scala-lang.org/license.html) | org.scala-lang # scala-reflect # 2.12.3 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-java8-compat_2.12 # 0.8.0 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-parser-combinators_2.12 # 1.0.4 | <notextile></notextile>
BSD | [BSD 3-clause](http://opensource.org/licenses/BSD-3-Clause) | org.scala-lang.modules # scala-xml_2.12 # 1.0.6 | <notextile></notextile>
BSD | [BSD License](http://www.opensource.org/licenses/bsd-license.php) | com.wix # wix-embedded-mysql # 2.1.4 | <notextile></notextile>
BSD | [BSD-2-Clause](https://jdbc.postgresql.org/about/license.html) | org.postgresql # postgresql # 42.0.0 | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.archive # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [Eclipse Distribution License (New BSD License)](null) | org.eclipse.jgit # org.eclipse.jgit.http.server # 4.8.0.201706111038-r | <notextile></notextile>
BSD | [New BSD License](http://www.opensource.org/licenses/bsd-license.php) | org.hamcrest # hamcrest-core # 1.3 | <notextile></notextile>
BSD | [Revised BSD](http://www.jcraft.com/jsch/LICENSE.txt) | com.jcraft # jsch # 0.1.54 | <notextile></notextile>
BSD | [Two-clause BSD-style license](http://github.com/slick/slick/blob/master/LICENSE.txt) | com.typesafe.slick # slick_2.12 # 3.2.1 | <notextile></notextile>
CC0 | [CC0](http://creativecommons.org/publicdomain/zero/1.0/) | org.reactivestreams # reactive-streams # 1.0.0 | <notextile></notextile>
CDDL | [COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0](https://glassfish.dev.java.net/public/CDDLv1.0.html) | javax.activation # activation # 1.1.1 | <notextile></notextile>
GPL | [CDDL/GPLv2+CE](https://glassfish.java.net/public/CDDL+GPL_1_1.html) | com.sun.mail # javax.mail # 1.5.2 | <notextile></notextile>
GPL with Classpath Extension | [CDDL + GPLv2 with classpath exception](https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html) | javax.servlet # javax.servlet-api # 3.1.0 | <notextile></notextile>
LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-classic # 1.2.3 | <notextile></notextile>
LGPL | [GNU Lesser General Public License](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) | ch.qos.logback # logback-core # 1.2.3 | <notextile></notextile>
LGPL | [LGPL, version 2.1](http://www.gnu.org/licenses/licenses.html) | net.java.dev.jna # jna # 4.0.0 | <notextile></notextile>
LGPL | [LGPL, version 2.1](http://www.gnu.org/licenses/licenses.html) | net.java.dev.jna # jna-platform # 4.0.0 | <notextile></notextile>
LGPL | [LGPL-2.1](null) | org.mariadb.jdbc # mariadb-java-client # 2.0.3 | <notextile></notextile>
MIT | [MIT License](http://www.opensource.org/licenses/mit-license.php) | org.slf4j # slf4j-api # 1.7.25 | <notextile></notextile>
MIT | [The MIT License](http://www.opensource.org/licenses/mit-license.php) | com.github.zafarkhaja # java-semver # 0.9.0 | <notextile></notextile>
MIT | [The MIT License](https://jsoup.org/license) | org.jsoup # jsoup # 1.10.2 | <notextile></notextile>
MIT | [The MIT License](http://github.com/mockito/mockito/blob/master/LICENSE) | org.mockito # mockito-all # 1.10.19 | <notextile></notextile>
MIT | [The MIT License](http://github.com/mockito/mockito/blob/master/LICENSE) | org.mockito # mockito-core # 2.7.22 | <notextile></notextile>
MIT | [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html) | net.coobird # thumbnailator # 0.4.8 | <notextile></notextile>
Mozilla | [MPL 2.0 or EPL 1.0](http://h2database.com/html/license.html) | com.h2database # h2 # 1.4.195 | <notextile></notextile>
Mozilla | [Mozilla Public License 1.1 (MPL 1.1)](http://www.mozilla.org/MPL/MPL-1.1.html) | com.googlecode.juniversalchardet # juniversalchardet # 1.0.3 | <notextile></notextile>
Public Domain | [Public Domain](http://en.wikipedia.org/wiki/Public_domain) | net.i2p.crypto # eddsa # 0.1.0 | <notextile></notextile>
unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcpkix-jdk15on # 1.56 | <notextile></notextile>
unrecognized | [Bouncy Castle Licence](http://www.bouncycastle.org/licence.html) | org.bouncycastle # bcprov-jdk15on # 1.56 | <notextile></notextile>
unrecognized | [Eclipse Public License 1.0](http://www.eclipse.org/legal/epl-v10.html) | junit # junit # 4.12 | <notextile></notextile>
unrecognized | [The OpenLDAP Public License](http://www.openldap.org/software/release/license.html) | com.novell.ldap # jldap # 2009-10-07 | <notextile></notextile>

24
doc/notification.md Normal file
View File

@@ -0,0 +1,24 @@
Notification Email
========
GitBucket can send email notification to users if this feature is enabled by an administrator.
The timing of the notification are as follows:
##### at the issue registration (new issue, new pull request)
When a record is saved into the ```ISSUE``` table, GitBucket does the notification.
##### at the comment registration
Among the records in the ```ISSUE_COMMENT``` table, them to be counted as a comment (i.e. the record ```ACTION``` column value is "comment" or "close_comment" or "reopen_comment") are saved, GitBucket does the notification.
##### at the status update (close, reopen, merge)
When the ```CLOSED``` column value is updated, GitBucket does the notification.
Notified users are as follows:
* individual repository's owner
* group members of group repository
* collaborators
* participants
However, the person performing the operation is excluded from the notification.

View File

@@ -6,7 +6,7 @@ Developer's Guide
* [Authentication in Controller](authenticator.md) * [Authentication in Controller](authenticator.md)
* [About Action in Issue Comment](comment_action.md) * [About Action in Issue Comment](comment_action.md)
* [Activity Types](activity.md) * [Activity Types](activity.md)
* [Notification Email](notification.md)
* [Automatic Schema Updating](auto_update.md) * [Automatic Schema Updating](auto_update.md)
* [Release Operation](release.md) * [Release Operation](release.md)
* [JRebel integration (optional)](jrebel.md) * [JRebel integration (optional)](jrebel.md)
* [Licenses](licenses.md)

View File

@@ -1,41 +0,0 @@
[
{
"id": "notifications",
"name": "Notifications Plugin",
"description": "Provides notifications feature on GitBucket.",
"versions": [
{
"version": "1.4.0",
"range": ">=4.19.0",
"file": "gitbucket-notifications-plugin_2.12-1.4.0.jar"
}
],
"default": true
},
{
"id": "emoji",
"name": "Emoji Plugin",
"description": "Provides Emoji support for GitBucket.",
"versions": [
{
"version": "4.5.0",
"range": ">=4.18.0",
"file": "gitbucket-emoji-plugin_2.12-4.5.0.jar"
}
],
"default": false
},
{
"id": "gist",
"name": "Gist Plugin",
"description": "Provides Gist feature on GitBucket.",
"versions": [
{
"version": "4.11.0",
"range": ">=4.19.0",
"file": "gitbucket-gist-plugin-assembly-4.11.0.jar"
}
],
"default": false
}
]

View File

@@ -1,13 +1,13 @@
import java.security.MessageDigest import java.security.MessageDigest;
import scala.annotation._ import scala.annotation._
import sbt._ import sbt._
import io._ import sbt.Using._
object Checksums { object Checksums {
private val bufferSize = 2048 private val bufferSize = 2048
def generate(source:File, target:File, algorithm:String):Unit = def generate(source:File, target:File, algorithm:String):Unit =
sbt.IO write (target, compute(source, algorithm)) IO write (target, compute(source, algorithm))
def compute(file:File, algorithm:String):String = def compute(file:File, algorithm:String):String =
hex(raw(file, algorithm)) hex(raw(file, algorithm))

View File

@@ -1,21 +0,0 @@
import com.eclipsesource.json.Json
import scala.collection.JavaConverters._
object PluginsJson {
def parse(json: String): Seq[(String, String, String)] = {
val value = Json.parse(json)
value.asArray.values.asScala.map { plugin =>
val pluginObject = plugin.asObject
val pluginName = "gitbucket-" + pluginObject.get("id").asString + "-plugin"
val latestVersionObject = pluginObject.get("versions").asArray.asScala.head.asObject
val file = latestVersionObject.get("file").asString
val version = latestVersionObject.get("version").asString
(pluginName, version, file)
}
}
}

View File

@@ -1 +1 @@
sbt.version=1.0.4 sbt.version=0.13.15

View File

@@ -1 +0,0 @@
libraryDependencies += "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.4"

View File

@@ -1,10 +1,8 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.12") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
//addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.0") addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.1")
//addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0") addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "1.0.1") addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC13")
addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0")

View File

@@ -1 +1 @@
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC11") addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")

View File

@@ -40,7 +40,7 @@ public class JettyLauncher {
} }
break; break;
case "--max_file_size": case "--max_file_size":
System.setProperty("gitbucket.maxFileSize", dim[1]); System.setProperty("gitbucket.maxFileSize", dim[2]);
break; break;
case "--gitbucket.home": case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]); System.setProperty("gitbucket.home", dim[1]);
@@ -48,12 +48,6 @@ public class JettyLauncher {
case "--temp_dir": case "--temp_dir":
tmpDirPath = dim[1]; tmpDirPath = dim[1];
break; break;
case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]);
break;
case "--validate_password":
System.setProperty("gitbucket.validate.password", dim[1]);
break;
} }
} }
} }

View File

@@ -25,32 +25,30 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter) context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*") context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
context.addFilter("ghCompatRepositoryAccessFilter", new GHCompatRepositoryAccessFilter)
context.getFilterRegistration("ghCompatRepositoryAccessFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Register controllers // Register controllers
context.mount(new PreProcessController, "/*") context.mount(new AnonymousAccessController, "/*")
context.addFilter("pluginControllerFilter", new PluginControllerFilter) PluginRegistry().getControllers.foreach { case (controller, path) =>
context.getFilterRegistration("pluginControllerFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.mount(controller, path)
}
context.mount(new IndexController, "/")
context.mount(new ApiController, "/api/v3")
context.mount(new FileUploadController, "/upload") context.mount(new FileUploadController, "/upload")
context.mount(new SystemSettingsController, "/admin")
val filter = new CompositeScalatraFilter() context.mount(new DashboardController, "/*")
filter.mount(new IndexController, "/") context.mount(new AccountController, "/*")
filter.mount(new ApiController, "/api/v3") context.mount(new RepositoryViewerController, "/*")
filter.mount(new SystemSettingsController, "/admin") context.mount(new WikiController, "/*")
filter.mount(new DashboardController, "/*") context.mount(new LabelsController, "/*")
filter.mount(new AccountController, "/*") context.mount(new PrioritiesController, "/*")
filter.mount(new RepositoryViewerController, "/*") context.mount(new MilestonesController, "/*")
filter.mount(new WikiController, "/*") context.mount(new IssuesController, "/*")
filter.mount(new LabelsController, "/*") context.mount(new PullRequestsController, "/*")
filter.mount(new PrioritiesController, "/*") context.mount(new RepositorySettingsController, "/*")
filter.mount(new MilestonesController, "/*")
filter.mount(new IssuesController, "/*")
filter.mount(new PullRequestsController, "/*")
filter.mount(new RepositorySettingsController, "/*")
context.addFilter("compositeScalatraFilter", filter)
context.getFilterRegistration("compositeScalatraFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Create GITBUCKET_HOME directory if it does not exist // Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(Directory.GitBucketHome) val dir = new java.io.File(Directory.GitBucketHome)

View File

@@ -39,12 +39,5 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new LiquibaseMigration("update/gitbucket-core_4.14.xml"), new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new SqlMigration("update/gitbucket-core_4.14.sql") new SqlMigration("update/gitbucket-core_4.14.sql")
), ),
new Version("4.14.1"), new Version("4.14.1")
new Version("4.15.0"),
new Version("4.16.0"),
new Version("4.17.0"),
new Version("4.18.0"),
new Version("4.19.0"),
new Version("4.19.1"),
new Version("4.19.2")
) )

View File

@@ -1,124 +0,0 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo}
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import ApiCommits._
case class ApiCommits(
url: ApiPath,
sha: String,
html_url: ApiPath,
comment_url: ApiPath,
commit: Commit,
author: ApiUser,
committer: ApiUser,
parents: Seq[Tree],
stats: Stats,
files: Seq[File]
)
object ApiCommits {
case class Commit(
url: ApiPath,
author: ApiPersonIdent,
committer: ApiPersonIdent,
message: String,
comment_count: Int,
tree: Tree
)
case class Tree(
url: ApiPath,
sha: String
)
case class Stats(
additions: Int,
deletions: Int,
total: Int
)
case class File(
filename: String,
additions: Int,
deletions: Int,
changes: Int,
status: String,
raw_url: ApiPath,
blob_url: ApiPath,
patch: String
)
def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account,
commentCount: Int): ApiCommits = {
val files = diffs.map { diff =>
var additions = 0
var deletions = 0
diff.patch.getOrElse("").split("\n").foreach { line =>
if(line.startsWith("+")) additions = additions + 1
if(line.startsWith("-")) deletions = deletions + 1
}
File(
filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath },
additions = additions,
deletions = deletions,
changes = additions + deletions,
status = diff.changeType match {
case ChangeType.ADD => "added"
case ChangeType.MODIFY => "modified"
case ChangeType.DELETE => "deleted"
case ChangeType.RENAME => "renamed"
case ChangeType.COPY => "copied"
},
raw_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}")
},
blob_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}")
},
patch = diff.patch.getOrElse("")
)
}
ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""),
commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
author = ApiPersonIdent.author(commitInfo),
committer = ApiPersonIdent.committer(commitInfo),
message = commitInfo.shortMessage,
comment_count = commentCount,
tree = Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet.
sha = commitInfo.id
)
),
author = ApiUser(author),
committer = ApiUser(committer),
parents = commitInfo.parents.map { parent =>
Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet.
sha = parent
)
},
stats = Stats(
additions = files.map(_.additions).sum,
deletions = files.map(_.deletions).sum,
total = files.map(_.additions).sum + files.map(_.deletions).sum
),
files = files
)
}
}

View File

@@ -1,13 +1,6 @@
package gitbucket.core.api package gitbucket.core.api
/** /**
* Path for API url. * path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
* If set path '/repos/aa/bb' then, expand 'http://server:port/repos/aa/bb' when converted to json.
*/ */
case class ApiPath(path: String) case class ApiPath(path: String)
/**
* Path for git repository via SSH.
* If set path '/aa/bb.git' then, expand 'git@server:port/aa/bb.git' when converted to json.
*/
case class SshPath(path: String)

View File

@@ -3,6 +3,7 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest} import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest}
import java.util.Date import java.util.Date
/** /**
* https://developer.github.com/v3/pulls/ * https://developer.github.com/v3/pulls/
*/ */
@@ -18,8 +19,7 @@ case class ApiPullRequest(
merged_by: Option[ApiUser], merged_by: Option[ApiUser],
title: String, title: String,
body: String, body: String,
user: ApiUser, user: ApiUser) {
assignee: Option[ApiUser]){
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}") val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff") //val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch") //val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -39,7 +39,6 @@ object ApiPullRequest{
headRepo: ApiRepository, headRepo: ApiRepository,
baseRepo: ApiRepository, baseRepo: ApiRepository,
user: ApiUser, user: ApiUser,
assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)] mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest = ): ApiPullRequest =
ApiPullRequest( ApiPullRequest(
@@ -60,16 +59,14 @@ object ApiPullRequest{
merged_by = mergedComment.map { case (_, account) => ApiUser(account) }, merged_by = mergedComment.map { case (_, account) => ApiUser(account) },
title = issue.title, title = issue.title,
body = issue.content.getOrElse(""), body = issue.content.getOrElse(""),
user = user, user = user
assignee = assignee
) )
case class Commit( case class Commit(
sha: String, sha: String,
ref: String, ref: String,
repo: ApiRepository)(baseOwner:String){ repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref } else { s"${repo.owner.login}:${ref}" } val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val user = repo.owner val user = repo.owner
} }
} }

View File

@@ -24,7 +24,6 @@ case class ApiRepository(
val http_url = ApiPath(s"/git/${full_name}.git") val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git") val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}") val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git"))
} }
object ApiRepository{ object ApiRepository{
@@ -56,13 +55,12 @@ object ApiRepository{
def forDummyPayload(owner: ApiUser): ApiRepository = def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository( ApiRepository(
name = "dummy", name="dummy",
full_name = s"${owner.login}/dummy", full_name=s"${owner.login}/dummy",
description = "", description="",
watchers = 0, watchers=0,
forks = 0, forks=0,
`private` = false, `private`=false,
default_branch = "master", default_branch="master",
owner = owner owner=owner)(true)
)(true)
} }

View File

@@ -1,24 +1,23 @@
package gitbucket.core.api package gitbucket.core.api
import java.time._ import org.joda.time.DateTime
import java.time.format.DateTimeFormatter import org.joda.time.DateTimeZone
import java.util.Date import org.joda.time.format._
import scala.util.Try
import org.json4s._ import org.json4s._
import org.json4s.jackson.Serialization import org.json4s.jackson.Serialization
import java.util.Date
import scala.util.Try
object JsonFormat { object JsonFormat {
case class Context(baseUrl: String, sshUrl: Option[String]) case class Context(baseUrl: String)
val parserISO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format => val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
( (
{ case JString(s) => Try(Date.from(Instant.parse(s))).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) }, { case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(OffsetDateTime.ofInstant(x.toInstant, ZoneId.of("UTC")).format(parserISO)) } { case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
) )
) + FieldSerializer[ApiUser]() + ) + FieldSerializer[ApiUser]() +
FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiPullRequest]() +
@@ -34,31 +33,23 @@ object JsonFormat {
FieldSerializer[ApiComment]() + FieldSerializer[ApiComment]() +
FieldSerializer[ApiContents]() + FieldSerializer[ApiContents]() +
FieldSerializer[ApiLabel]() + FieldSerializer[ApiLabel]() +
FieldSerializer[ApiCommits]() +
FieldSerializer[ApiCommits.Commit]() +
FieldSerializer[ApiCommits.Tree]() +
FieldSerializer[ApiCommits.Stats]() +
FieldSerializer[ApiCommits.File]() +
ApiBranchProtection.enforcementLevelSerializer ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => ({ def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length)) (
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") {
}, { case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case ApiPath(path) => JString(c.baseUrl + path) case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
})) },
{
def sshPathSerializer(c: Context) = new CustomSerializer[SshPath](_ => ({ case ApiPath(path) => JString(c.baseUrl + path)
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => SshPath(s.substring(c.sshUrl.get.length)) }
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath") )
}, { )
case SshPath(path) => c.sshUrl.map { sshUrl => JString(sshUrl + path) } getOrElse JNothing
}))
/** /**
* convert object to json string * convert object to json string
*/ */
def apply(obj: AnyRef)(implicit c: Context): String = def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
Serialization.write(obj)(jsonFormats + apiPathSerializer(c) + sshPathSerializer(c))
} }

View File

@@ -12,10 +12,10 @@ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._ import gitbucket.core.util.StringUtil._
import gitbucket.core.util._ import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.BadRequest import org.scalatra.BadRequest
import org.scalatra.forms._
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
@@ -137,17 +137,16 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
private def accountWebhookEvents = new ValueType[Set[WebHook.Event]]{ private def accountWebhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = { def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t => WebHook.Event.values.flatMap { t =>
params.optionValue(name + "." + t.name).map(_ => t) params.get(name + "." + t.name).map(_ => t)
}.toSet }.toSet
} }
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
if(convert(name, params, messages).isEmpty){ Seq(name -> messages("error.required").format(name))
Seq(name -> messages("error.required").format(name)) } else {
} else { Nil
Nil }
}
} }
@@ -233,10 +232,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} getOrElse NotFound() } getOrElse NotFound()
}) })
get("/captures/(.*)".r) {
multiParams("captures").head
}
get("/:userName/_delete")(oneselfOnly { get("/:userName/_delete")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
@@ -636,14 +631,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} }
private def uniqueRepository: Constraint = new Constraint(){ private def uniqueRepository: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = { override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
for { params.get("owner").flatMap { userName =>
userName <- params.optionValue("owner") getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
} yield {
"Repository already exists."
} }
}
} }
private def members: Constraint = new Constraint(){ private def members: Constraint = new Constraint(){

View File

@@ -0,0 +1,14 @@
package gitbucket.core.controller
class AnonymousAccessController extends AnonymousAccessControllerBase
trait AnonymousAccessControllerBase extends ControllerBase {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -5,16 +5,14 @@ import gitbucket.core.model._
import gitbucket.core.service.IssuesService.IssueSearchCondition import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService._ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup} import gitbucket.core.util.Implicits._
import gitbucket.core.view.helpers.{renderMarkup, isRenderable}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk import org.scalatra.{NoContent, UnprocessableEntity, Created}
import org.scalatra.{Created, NoContent, UnprocessableEntity}
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class ApiController extends ApiControllerBase class ApiController extends ApiControllerBase
@@ -51,7 +49,6 @@ trait ApiControllerBase extends ControllerBase {
with LabelsService with LabelsService
with MilestonesService with MilestonesService
with PullRequestService with PullRequestService
with CommitsService
with CommitStatusService with CommitStatusService
with RepositoryCreationService with RepositoryCreationService
with IssueCreationService with IssueCreationService
@@ -127,10 +124,10 @@ trait ApiControllerBase extends ControllerBase {
/** /**
* https://developer.github.com/v3/repos/branches/#get-branch * https://developer.github.com/v3/repos/branches/#get-branch
*/ */
get ("/api/v3/repos/:owner/:repo/branches/*")(referrersOnly { repository => get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository =>
//import gitbucket.core.api._ //import gitbucket.core.api._
(for{ (for{
branch <- params.get("splat") if repository.branchList.contains(branch) branch <- params.get("branch") if repository.branchList.contains(branch)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield { } yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
@@ -289,10 +286,10 @@ trait ApiControllerBase extends ControllerBase {
/** /**
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/ */
patch("/api/v3/repos/:owner/:repo/branches/*")(ownerOnly { repository => patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._ import gitbucket.core.api._
(for{ (for{
branch <- params.get("splat") if repository.branchList.contains(branch) branch <- params.get("branch") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch) br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield { } yield {
@@ -384,7 +381,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{ (for{
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId) comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield { } yield {
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}) getOrElse NotFound() }) getOrElse NotFound()
@@ -501,7 +498,7 @@ trait ApiControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = val issues: List[(Issue, Account, Int, PullRequest, Repository, Account)] =
searchPullRequestByApi( searchPullRequestByApi(
condition = condition, condition = condition,
offset = (page - 1) * PullRequestLimit, offset = (page - 1) * PullRequestLimit,
@@ -509,14 +506,13 @@ trait ApiControllerBase extends ControllerBase {
repos = repository.owner -> repository.name repos = repository.owner -> repository.name
) )
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) => JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
ApiPullRequest( ApiPullRequest(
issue = issue, issue = issue,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)), headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)), baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser), user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
) )
}) })
@@ -533,7 +529,6 @@ trait ApiControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
JsonFormat(ApiPullRequest( JsonFormat(ApiPullRequest(
@@ -542,7 +537,6 @@ trait ApiControllerBase extends ControllerBase {
headRepo = ApiRepository(headRepo, ApiUser(headOwner)), headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)), baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser), user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId) mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)) ))
}) getOrElse NotFound() }) getOrElse NotFound()
@@ -633,52 +627,6 @@ trait ApiControllerBase extends ControllerBase {
}) getOrElse NotFound() }) getOrElse NotFound()
}) })
/**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit
*/
get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params("sha")
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = using(new RevWalk(repo)){ revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
))
}
})
private def getAccount(userName: String, email: String): Account = {
getAccountByMailAddress(email).getOrElse {
Account(
userName = userName,
fullName = userName,
mailAddress = email,
password = "xxx",
isAdmin = false,
url = None,
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = true,
description = None
)
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName

View File

@@ -9,11 +9,12 @@ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import io.github.gitbucket.scalatra.forms._
import org.json4s._ import org.json4s._
import org.scalatra._ import org.scalatra._
import org.scalatra.i18n._ import org.scalatra.i18n._
import org.scalatra.json._ import org.scalatra.json._
import org.scalatra.forms._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import javax.servlet.{FilterChain, ServletRequest, ServletResponse} import javax.servlet.{FilterChain, ServletRequest, ServletResponse}
@@ -25,17 +26,14 @@ import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
/** /**
* Provides generic features for controller implementations. * Provides generic features for controller implementations.
*/ */
abstract class ControllerBase extends ScalatraFilter abstract class ControllerBase extends ScalatraFilter
with ValidationSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService { with SystemSettingsService {
private val logger = LoggerFactory.getLogger(getClass)
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
before("/api/v3/*") { before("/api/v3/*") {
@@ -149,20 +147,6 @@ abstract class ControllerBase extends ScalatraFilter
} }
} }
error{
case e => {
logger.error(s"Catch unhandled error in request: ${request}", e)
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.InternalServerError()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.InternalServerError(ApiError("Internal Server Error"))
} else {
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
}
}
}
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true, includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true) absolutize: Boolean = true, withSessionId: Boolean = true)
@@ -176,10 +160,10 @@ abstract class ControllerBase extends ScalatraFilter
protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){ protected def trim2[T](valueType: SingleValueType[T]): SingleValueType[T] = new SingleValueType[T](){
def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages) def convert(value: String, messages: Messages): T = valueType.convert(trim(value), messages)
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Seq[(String, String)] =
valueType.validate(name, trim(value), params, messages) valueType.validate(name, trim(value), params, messages)
private def trim(value: String): String = if(value == null) null else value.replace("\r\n", "").trim private def trim(value: String): String = if(value == null) null else value.replaceAll("\r\n", "").trim
} }
/** /**
@@ -314,14 +298,13 @@ trait AccountManagementControllerBase extends ControllerBase {
} }
protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){ protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = { override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
getAccountByMailAddress(value, true) getAccountByMailAddress(value, true)
.filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.optionValue(paramName) } .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) }
.map { _ => "Mail address is already registered." } .map { _ => "Mail address is already registered." }
}
} }
val allReservedNames = Set("git", "admin", "upload", "api", "assets", "plugin-assets", "signin", "signout", "register", "activities.atom", "sidebar-collapse", "groups", "new") val allReservedNames = Set("git", "admin", "upload", "api")
protected def reservedNames(): Constraint = new Constraint(){ protected def reservedNames(): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){ override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
Some(s"${value} is reserved") Some(s"${value} is reserved")

View File

@@ -1,6 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.{AccountService, RepositoryService} import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.servlet.Database import gitbucket.core.servlet.Database
import gitbucket.core.util._ import gitbucket.core.util._
@@ -47,7 +48,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
FileUtils.writeByteArrayToFile(new java.io.File( FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")), getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get) fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}, _ => true) }, FileUtil.isUploadableType)
} }
post("/wiki/:owner/:repository"){ post("/wiki/:owner/:repository"){
@@ -79,12 +80,12 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
builder.finish() builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, s"Uploaded ${fileName}") Constants.HEAD, loginAccount.userName, loginAccount.mailAddress, s"Uploaded ${fileName}")
fileName fileName
} }
} }
}, _ => true) }, FileUtil.isUploadableType)
} }
} getOrElse BadRequest() } getOrElse BadRequest()
} }
@@ -112,7 +113,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
} }
} }
private def execute(f: (FileItem, String) => Unit , mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match { private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) => case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId => defining(FileUtil.generateFileId){ fileId =>
f(file, fileId) f(file, fileId)

View File

@@ -6,7 +6,7 @@ import gitbucket.core.service._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator} import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator}
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok import org.scalatra.Ok
@@ -19,12 +19,11 @@ trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with RepositorySearchService self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator => with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String, hash: Option[String]) case class SignInForm(userName: String, password: String)
val signinForm = mapping( val signinForm = mapping(
"userName" -> trim(label("Username", text(required))), "userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required))), "password" -> trim(label("Password", text(required)))
"hash" -> trim(optional(text()))
)(SignInForm.apply) )(SignInForm.apply)
// val searchForm = mapping( // val searchForm = mapping(
@@ -55,7 +54,7 @@ trait IndexControllerBase extends ControllerBase {
post("/signin", signinForm){ form => post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match { authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account, form.hash) case Some(account) => signin(account)
case None => { case None => {
flash += "userName" -> form.userName flash += "userName" -> form.userName
flash += "password" -> form.password flash += "password" -> form.password
@@ -75,7 +74,7 @@ trait IndexControllerBase extends ControllerBase {
xml.feed(getRecentActivities()) xml.feed(getRecentActivities())
} }
post("/sidebar-collapse"){ get("/sidebar-collapse"){
if(params("collapse") == "true"){ if(params("collapse") == "true"){
session.setAttribute("sidebar-collapse", "true") session.setAttribute("sidebar-collapse", "true")
} else { } else {
@@ -87,7 +86,7 @@ trait IndexControllerBase extends ControllerBase {
/** /**
* Set account information into HttpSession and redirect. * Set account information into HttpSession and redirect.
*/ */
private def signin(account: Account, hash: Option[String]) = { private def signin(account: Account) = {
session.setAttribute(Keys.Session.LoginAccount, account) session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName) updateLastLoginDate(account.userName)
@@ -99,7 +98,7 @@ trait IndexControllerBase extends ControllerBase {
if(redirectUrl.stripSuffix("/") == request.getContextPath){ if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/") redirect("/")
} else { } else {
redirect(redirectUrl + hash.getOrElse("")) redirect(redirectUrl)
} }
}.getOrElse { }.getOrElse {
redirect("/") redirect("/")
@@ -121,12 +120,7 @@ trait IndexControllerBase extends ControllerBase {
case (true, false) => !t.isGroupAccount case (true, false) => !t.isGroupAccount
case (false, true) => t.isGroupAccount case (false, true) => t.isGroupAccount
case (false, false) => false case (false, false) => false
}}.map { t => }}.map { t => t.userName }
Map(
"label" -> s"<b>@${t.userName}</b> ${t.fullName}",
"value" -> t.userName
)
}
)) ))
) )
}) })

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.Markdown import gitbucket.core.view.Markdown
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.{BadRequest, Ok} import org.scalatra.{BadRequest, Ok}
@@ -193,7 +193,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){ if(isEditableContent(owner, name, comment.commentedUserName)){
updateComment(comment.issueId, comment.commentId, form.content) updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized() } else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
@@ -204,7 +204,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){ if(isEditableContent(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.issueId, comment.commentId)) Ok(deleteComment(comment.commentId))
} else Unauthorized() } else Unauthorized()
} getOrElse NotFound() } getOrElse NotFound()
} }

View File

@@ -4,8 +4,7 @@ import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok import org.scalatra.Ok
@@ -83,10 +82,10 @@ trait LabelsControllerBase extends ControllerBase {
} }
private def uniqueLabelName: Constraint = new Constraint(){ private def uniqueLabelName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = { override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params.value("owner") val owner = params("owner")
val repository = params.value("repository") val repository = params("repository")
params.optionValue("labelId").map { labelId => params.get("labelId").map { labelId =>
getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.") getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
}.getOrElse { }.getOrElse {
getLabel(owner, repository, value).map(_ => "Name has already been taken.") getLabel(owner, repository, value).map(_ => "Name has already been taken.")

View File

@@ -4,7 +4,7 @@ import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService} import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
class MilestonesController extends MilestonesControllerBase class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService with MilestonesService with RepositoryService with AccountService

View File

@@ -1,40 +0,0 @@
package gitbucket.core.controller
import org.scalatra.MovedPermanently
class PreProcessController extends PreProcessControllerBase
trait PreProcessControllerBase extends ControllerBase {
/**
* Provides GitHub compatible URLs for Git client.
*
* <ul>
* <li>git clone http://localhost:8080/owner/repo</li>
* <li>git clone http://localhost:8080/owner/repo.git</li>
* </ul>
*
* @see https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
*/
get("/*/*/info/refs") {
val query = Option(request.getQueryString).map("?" + _).getOrElse("")
halt(MovedPermanently(baseUrl + "/git" + request.getRequestURI + query))
}
/**
* Filter requests from anonymous users.
*
* If anonymous access is allowed, pass all requests.
* But if it's not allowed, demands authentication except some paths.
*/
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -4,8 +4,7 @@ import gitbucket.core.issues.priorities.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService} import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok import org.scalatra.Ok
@@ -99,10 +98,10 @@ trait PrioritiesControllerBase extends ControllerBase {
} }
private def uniquePriorityName: Constraint = new Constraint(){ private def uniquePriorityName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = { override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params.value("owner") val owner = params("owner")
val repository = params.value("repository") val repository = params("repository")
params.optionValue("priorityId").map { priorityId => params.get("priorityId").map { priorityId =>
getPriority(owner, repository, value).filter(_.priorityId != priorityId.toInt).map(_ => "Name has already been taken.") getPriority(owner, repository, value).filter(_.priorityId != priorityId.toInt).map(_ => "Name has already been taken.")
}.getOrElse { }.getOrElse {
getPriority(owner, repository, value).map(_ => "Name has already been taken.") getPriority(owner, repository, value).map(_ => "Name has already been taken.")

View File

@@ -13,7 +13,7 @@ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util._ import gitbucket.core.util._
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.PersonIdent
@@ -251,7 +251,7 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(owner, name))) { git => using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close. // mark issue as merged and close.
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge") createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close") createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true) updateClosed(owner, name, issueId, true)
@@ -282,10 +282,7 @@ trait PullRequestsControllerBase extends ControllerBase {
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get) callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// call hooks // call hooks
PluginRegistry().getPullRequestHooks.foreach{ h => PluginRegistry().getPullRequestHooks.foreach(_.merged(issue, repository))
h.addedComment(commentId, form.message, issue, repository)
h.merged(issue, repository)
}
redirect(s"/${owner}/${name}/pull/${issueId}") redirect(s"/${owner}/${name}/pull/${issueId}")
} }
@@ -324,8 +321,8 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository => get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
(for( (for(
originRepositoryName <- if(originOwner == forkedOwner) { originRepositoryName <- if(originOwner == forkedOwner) {
@@ -411,8 +408,8 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository => ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for( (for(
originRepositoryName <- if(originOwner == forkedOwner){ originRepositoryName <- if(originOwner == forkedOwner){
@@ -505,7 +502,7 @@ trait PullRequestsControllerBase extends ControllerBase {
* - "owner:branch" to ("owner", "branch") * - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch") * - "branch" to ("defaultOwner", "branch")
*/ */
private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) = private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){ if(value.contains(':')){
val array = value.split(":") val array = value.split(":")
(array(0), array(1)) (array(0), array(1))

View File

@@ -1,10 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.time.{LocalDateTime, ZoneId, ZoneOffset}
import java.util.Date
import gitbucket.core.settings.html import gitbucket.core.settings.html
import gitbucket.core.model.{RepositoryWebHook, WebHook} import gitbucket.core.model.{WebHook, RepositoryWebHook}
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
import gitbucket.core.util._ import gitbucket.core.util._
@@ -12,7 +9,7 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -153,7 +150,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/branches")(ownerOnly { repository => get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
val protecteions = getProtectedBranchList(repository.owner, repository.name) val protecteions = getProtectedBranchList(repository.owner, repository.name)
html.branches(repository, protecteions, flash.get("info")) html.branches(repository, protecteions, flash.get("info"))
}) });
/** Update default branch */ /** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
@@ -178,8 +175,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/settings/branches") redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else { } else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.of("UTC")))).toSet
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity) val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info")) html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
} }
@@ -347,12 +343,20 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name))
} }
} }
// Move files directory // Move lfs directory
defining(getRepositoryFilesDir(repository.owner, repository.name)){ dir => defining(getLfsDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) { if(dir.isDirectory()) {
FileUtils.moveDirectory(dir, getRepositoryFilesDir(form.newOwner, repository.name)) FileUtils.moveDirectory(dir, getLfsDir(form.newOwner, repository.name))
} }
} }
// Move attached directory
defining(getAttachedDir(repository.owner, repository.name)){ dir =>
if(dir.isDirectory) {
FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name))
}
}
// Delere parent directory
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
// Call hooks // Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.transferred(repository.owner, form.newOwner, repository.name)) PluginRegistry().getRepositoryHooks.foreach(_.transferred(repository.owner, form.newOwner, repository.name))
@@ -372,7 +376,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name))
FileUtils.deleteDirectory(getRepositoryFilesDir(repository.owner, repository.name)) val lfsDir = getLfsDir(repository.owner, repository.name)
FileUtils.deleteDirectory(lfsDir)
FileUtil.deleteDirectoryIfEmpty(lfsDir.getParentFile())
// Call hooks // Call hooks
PluginRegistry().getRepositoryHooks.foreach(_.deleted(repository.owner, repository.name)) PluginRegistry().getRepositoryHooks.foreach(_.deleted(repository.owner, repository.name))
@@ -387,7 +393,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/gc")(ownerOnly { repository => post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.gc().call() git.gc();
} }
} }
flash += "info" -> "Garbage collection has been executed." flash += "info" -> "Garbage collection has been executed."
@@ -429,12 +435,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
} }
private def webhookEvents = new ValueType[Set[WebHook.Event]]{ private def webhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = { def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t => WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t) params.get(name + "." + t.name).map(_ => t)
}.toSet }.toSet
} }
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){ def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
Seq(name -> messages("error.required").format(name)) Seq(name -> messages("error.required").format(name))
} else { } else {
Nil Nil
@@ -460,22 +466,19 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Duplicate check for the rename repository name. * Duplicate check for the rename repository name.
*/ */
private def renameRepositoryName: Constraint = new Constraint(){ private def renameRepositoryName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = { override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
for { params.get("repository").filter(_ != value).flatMap { _ =>
repoName <- params.optionValue("repository") if repoName != value params.get("owner").flatMap { userName =>
userName <- params.optionValue("owner") getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.")
_ <- getRepositoryNamesOfUser(userName).find(_ == value) }
} yield {
"Repository already exists."
} }
}
} }
/** /**
* *
*/ */
private def featureOption: Constraint = new Constraint(){ private def featureOption: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.") if(Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
} }

View File

@@ -13,18 +13,17 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState, CommitStatus, WebHook} import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import org.scalatra._ import org.scalatra._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -175,24 +174,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = repository.splitPath(multiParams("splat").head) val (branchName, path) = repository.splitPath(multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
def getStatuses(sha: String): List[CommitStatus] = {
getCommitStatues(repository.owner, repository.name, sha)
}
def getSummary(statuses: List[CommitStatus]): (CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
state -> summary
}
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match { JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) => case Right((logs, hasNext)) =>
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), getStatuses, getSummary) }, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound() case Left(_) => NotFound()
} }
} }
@@ -225,7 +213,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) => post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
val files = form.uploadFiles.split("\n").map { line => val files = form.uploadFiles.split("\n").map { line =>
val i = line.indexOf(':') val i = line.indexOf(":")
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim) CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
} }
@@ -234,7 +222,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
branch = form.branch, branch = form.branch,
path = form.path, path = form.path,
files = files, files = files,
message = form.message.getOrElse("Add files via upload") message = form.message.getOrElse(s"Add files via upload")
) )
if(form.path.length == 0){ if(form.path.length == 0){
@@ -321,7 +309,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
commit = form.commit commit = form.commit
) )
redirect(s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${ redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if (form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}" if (form.path.length == 0) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}") }")
}) })
@@ -432,7 +420,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
try { try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit => defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
JGitUtil.getDiffs(git, id, true) match { JGitUtil.getDiffs(git, id) match {
case (diffs, oldCommitId) => case (diffs, oldCommitId) =>
html.commit(id, new JGitUtil.CommitInfo(revCommit), html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
@@ -479,13 +467,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
form.issueId match { form.issueId match {
case Some(issueId) => case Some(issueId) =>
getPullRequest(repository.owner, repository.name, issueId).foreach { case (issue, pullRequest) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content) callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, form.content, issue, repository)) case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
callPullRequestReviewCommentWebHook("create", comment, repository, issue, pullRequest, context.baseUrl, context.loginAccount.get)
}
case None =>
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
} }
helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository) helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
}) })
@@ -646,8 +630,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
case class UploadFiles(branch: String, path: String, fileIds: Map[String,String], message: String) { case class UploadFiles(branch: String, path: String, fileIds : Map[String,String], message: String) {
lazy val isValid: Boolean = fileIds.nonEmpty lazy val isValid: Boolean = fileIds.size > 0
} }
case class CommitFile(id: String, name: String) case class CommitFile(id: String, name: String)
@@ -718,63 +702,39 @@ trait RepositoryViewerControllerBase extends ControllerBase {
f(git, headTip, builder, inserter) f(git, headTip, builder, inserter)
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
headName, loginAccount.fullName, loginAccount.mailAddress, message) headName, loginAccount.userName, loginAccount.mailAddress, message)
inserter.flush() inserter.flush()
inserter.close() inserter.close()
val receivePack = new ReceivePack(git.getRepository) // update refs
val receiveCommand = new ReceiveCommand(headTip, commitId, headName) val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
refUpdate.update()
// call post commit hook // update pull request
val error = PluginRegistry().getReceiveHooks.flatMap { hook => updatePullRequests(repository.owner, repository.name, branch)
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, loginAccount.userName)
}.headOption
error match { // record activity
case Some(error) => val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
// commit is rejected recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
// TODO Notify commit failure to edited user
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(headTip)
refUpdate.setForceUpdate(true)
refUpdate.update()
case None => // create issue comment by commit message
// update refs createIssueComment(repository.owner, repository.name, commitInfo)
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
refUpdate.update()
// update pull request // close issue by commit message
updatePullRequests(repository.owner, repository.name, branch) closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// record activity //call web hook
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo)) val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
// create issue comment by commit message getAccountByUserName(repository.owner).map{ ownerAccount =>
createIssueComment(repository.owner, repository.name, commitInfo) WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
// close issue by commit message }
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call post commit hook
PluginRegistry().getReceiveHooks.foreach { hook =>
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, loginAccount.userName)
}
//call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
} }
} }
} }

View File

@@ -6,19 +6,15 @@ import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService} import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.{AdminAuthenticator, Mailer} import gitbucket.core.util.{AdminAuthenticator, Mailer}
import gitbucket.core.ssh.SshServer import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository} import gitbucket.core.plugin.PluginRegistry
import SystemSettingsService._ import SystemSettingsService._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._ import gitbucket.core.util.StringUtil._
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.{FileUtils, IOUtils} import org.apache.commons.io.{FileUtils, IOUtils}
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import com.github.zafarkhaja.semver.{Version => Semver}
import gitbucket.core.GitBucketCoreModule
import scala.collection.JavaConverters._
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with RepositoryService with AdminAuthenticator with AccountService with RepositoryService with AdminAuthenticator
@@ -63,8 +59,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"tls" -> trim(label("Enable TLS", optional(boolean()))), "tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))), "ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text()))) "keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply)), )(Ldap.apply))
"skinName" -> trim(label("AdminLTE skin name", text(required)))
)(SystemSettings.apply).verifying { settings => )(SystemSettings.apply).verifying { settings =>
Vector( Vector(
if(settings.ssh && settings.baseUrl.isEmpty){ if(settings.ssh && settings.baseUrl.isEmpty){
@@ -174,13 +169,9 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/system/sendmail", sendMailForm)(adminOnly { form => post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try { try {
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send( new Mailer(form.smtp).send(form.testAddress,
to = form.testAddress, "Test message from GitBucket", "This is a test message from GitBucket.",
subject = "Test message from GitBucket", context.loginAccount.get)
textMsg = "This is a test message from GitBucket.",
htmlMsg = None,
loginAccount = context.loginAccount
)
"Test mail has been sent to: " + form.testAddress "Test mail has been sent to: " + form.testAddress
@@ -190,71 +181,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
}) })
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
// Installed plugins html.plugins(PluginRegistry().getPlugins())
val enabledPlugins = PluginRegistry().getPlugins()
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
// Plugins in the local repository
val repositoryPlugins = PluginRepository.getPlugins()
.filterNot { meta =>
enabledPlugins.exists { plugin => plugin.pluginId == meta.id &&
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
}
}.map { meta =>
(meta, meta.versions.reverse.find { version => gitbucketVersion.satisfies(version.range) })
}.collect { case (meta, Some(version)) =>
new PluginInfoBase(
pluginId = meta.id,
pluginName = meta.name,
pluginVersion = version.version,
description = meta.description
)
}
// Merge
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
html.plugins(plugins, flash.get("info"))
})
post("/admin/plugins/_reload")(adminOnly {
PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
flash += "info" -> "All plugins were reloaded."
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
PluginRegistry().getPlugins()
.collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin }
.foreach { _ =>
PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
flash += "info" -> s"${pluginId} was uninstalled."
}
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
/// TODO!!!!
PluginRepository.getPlugins()
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version) )}
.foreach { case (meta, version) =>
version.foreach { version =>
// TODO Install version!
PluginRegistry.install(
new java.io.File(PluginHome, s".repository/${version.file}"),
request.getServletContext,
loadSystemSettings(),
request2Session(request).conn
)
flash += "info" -> s"${pluginId} was installed."
}
}
redirect("/admin/plugins")
}) })

View File

@@ -1,91 +0,0 @@
package gitbucket.core.controller
import org.json4s.{JField, JObject, JString}
import org.scalatra._
import org.scalatra.json._
import org.scalatra.forms._
import org.scalatra.i18n.I18nSupport
import org.scalatra.servlet.ServletBase
/**
* Extends scalatra-forms to support the client-side validation and Ajax requests as well.
*/
trait ValidationSupport extends FormSupport { self: ServletBase with JacksonJsonSupport with I18nSupport =>
def get[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
get(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def post[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
post(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def put[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
put(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def delete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
registerValidate(path, form)
delete(path){
validate(form)(errors => BadRequest(), form => action(form))
}
}
def ajaxGet[T](path: String, form: ValueType[T])(action: T => Any): Route = {
get(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxPost[T](path: String, form: ValueType[T])(action: T => Any): Route = {
post(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxDelete[T](path: String, form: ValueType[T])(action: T => Any): Route = {
delete(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
def ajaxPut[T](path: String, form: ValueType[T])(action: T => Any): Route = {
put(path){
validate(form)(errors => ajaxError(errors), form => action(form))
}
}
private def registerValidate[T](path: String, form: ValueType[T]) = {
post(path.replaceFirst("/$", "") + "/validate"){
contentType = "application/json"
toJson(form.validate("", multiParams, messages))
}
}
/**
* Responds errors for ajax requests.
*/
private def ajaxError(errors: Seq[(String, String)]): JObject = {
status = 400
contentType = "application/json"
toJson(errors)
}
/**
* Converts errors to JSON.
*/
private def toJson(errors: Seq[(String, String)]): JObject =
JObject(errors.map { case (key, value) =>
JField(key, JString(value))
}.toList)
}

View File

@@ -10,7 +10,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -76,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true, false).filter(_.newPath == pageName + ".md"), repository, html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
isEditable(repository), flash.get("info")) isEditable(repository), flash.get("info"))
} }
}) })
@@ -85,7 +85,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true, false), repository, html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
isEditable(repository), flash.get("info")) isEditable(repository), flash.get("info"))
} }
}) })
@@ -226,8 +226,8 @@ trait WikiControllerBase extends ControllerBase {
}) })
private def unique: Constraint = new Constraint(){ private def unique: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, Seq[String]], messages: Messages): Option[String] = override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] =
getWikiPageList(params.value("owner"), params.value("repository")).find(_ == value).map(_ => "Page already exists.") getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.")
} }
private def pagename: Constraint = new Constraint(){ private def pagename: Constraint = new Constraint(){

View File

@@ -3,20 +3,18 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.Issue import gitbucket.core.model.Issue
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model.Profile._
import profile.api._
trait IssueHook { trait IssueHook {
def created(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def created(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def closed(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def reopened(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
} }
trait PullRequestHook extends IssueHook { trait PullRequestHook extends IssueHook {
def merged(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def merged(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
} }

View File

@@ -8,7 +8,6 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import io.github.gitbucket.solidbase.model.Version import io.github.gitbucket.solidbase.model.Version
import org.apache.sshd.server.Command
import play.twirl.api.Html import play.twirl.api.Html
/** /**
@@ -122,16 +121,6 @@ abstract class Plugin {
*/ */
def pullRequestHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PullRequestHook] = Nil def pullRequestHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PullRequestHook] = Nil
/**
* Override to add repository headers.
*/
val repositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add repository headers.
*/
def repositoryHeaders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/** /**
* Override to add global menus. * Override to add global menus.
*/ */
@@ -242,17 +231,6 @@ abstract class Plugin {
*/ */
def suggestionProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[SuggestionProvider] = Nil def suggestionProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[SuggestionProvider] = Nil
/**
* Override to add ssh command providers.
*/
val sshCommandProviders: Seq[PartialFunction[String, Command]] = Nil
/**
* Override to add ssh command providers.
*/
def sshCommandProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PartialFunction[String, Command]] = Nil
/** /**
* This method is invoked in initialization of plugin system. * This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry. * Register plugin functionality to PluginRegistry.
@@ -288,9 +266,6 @@ abstract class Plugin {
(pullRequestHooks ++ pullRequestHooks(registry, context, settings)).foreach { pullRequestHook => (pullRequestHooks ++ pullRequestHooks(registry, context, settings)).foreach { pullRequestHook =>
registry.addPullRequestHook(pullRequestHook) registry.addPullRequestHook(pullRequestHook)
} }
(repositoryHeaders ++ repositoryHeaders(registry, context, settings)).foreach { repositoryHeader =>
registry.addRepositoryHeader(repositoryHeader)
}
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu => (globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
registry.addGlobalMenu(globalMenu) registry.addGlobalMenu(globalMenu)
} }
@@ -312,8 +287,8 @@ abstract class Plugin {
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab => (dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
registry.addDashboardTab(dashboardTab) registry.addDashboardTab(dashboardTab)
} }
(issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebarComponent => (issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebar =>
registry.addIssueSidebar(issueSidebarComponent) registry.addIssueSidebar(issueSidebar)
} }
(assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping => (assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping =>
registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader)) registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader))
@@ -324,23 +299,14 @@ abstract class Plugin {
(suggestionProviders ++ suggestionProviders(registry, context, settings)).foreach { suggestionProvider => (suggestionProviders ++ suggestionProviders(registry, context, settings)).foreach { suggestionProvider =>
registry.addSuggestionProvider(suggestionProvider) registry.addSuggestionProvider(suggestionProvider)
} }
(sshCommandProviders ++ sshCommandProviders(registry, context, settings)).foreach { sshCommandProvider =>
registry.addSshCommandProvider(sshCommandProvider)
}
} }
/** /**
* This method is invoked when the plugin system is shutting down. * This method is invoked in shutdown of plugin system.
* If the plugin has any resources, release them in this method. * If the plugin has any resources, release them in this method.
*/ */
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {} def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
// /**
// * This method is invoked when this plugin is uninstalled.
// * Cleanup database or any other resources in this method if necessary.
// */
// def uninstall(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
/** /**
* Helper method to get a resource from classpath. * Helper method to get a resource from classpath.
*/ */

View File

@@ -0,0 +1,258 @@
package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import java.util.Base64
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.slf4j.LoggerFactory
import play.twirl.api.Html
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
class PluginRegistry {
private val plugins = new ListBuffer[PluginInfo]
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
private val renderers = mutable.Map[String, Renderer]()
renderers ++= Seq(
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
private val accountHooks = new ListBuffer[AccountHook]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
private val repositoryHooks = new ListBuffer[RepositoryHook]
private val issueHooks = new ListBuffer[IssueHook]
issueHooks += new gitbucket.core.util.Notifier.IssueHook()
private val pullRequestHooks = new ListBuffer[PullRequestHook]
pullRequestHooks += new gitbucket.core.util.Notifier.PullRequestHook()
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
private val issueSidebars = new ListBuffer[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
private val suggestionProviders = new ListBuffer[SuggestionProvider]
suggestionProviders += new UserNameSuggestionProvider()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
def getPlugins(): List[PluginInfo] = plugins.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = Base64.getEncoder.encodeToString(bytes)
images += ((id, encoded))
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
addImage(id, bytes)
}
def getImage(id: String): String = images(id)
def addController(path: String, controller: ControllerBase): Unit = controllers += ((controller, path))
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
def addJavaScript(path: String, script: String): Unit = javaScripts += ((path, script))
def getJavaScript(currentPath: String): List[String] = javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer))
def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings += routing
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.toSeq
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
case GitRepositoryRouting(urlPath, _, _) => {
repositoryPath.matches("/" + urlPath + "(/.*)?")
}
}
}
def addAccountHook(accountHook: AccountHook): Unit = accountHooks += accountHook
def getAccountHooks: Seq[AccountHook] = accountHooks.toSeq
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks += repositoryHook
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.toSeq
def addIssueHook(issueHook: IssueHook): Unit = issueHooks += issueHook
def getIssueHooks: Seq[IssueHook] = issueHooks.toSeq
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks += pullRequestHook
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus += repositoryMenu
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs += repositorySettingTab
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs += profileTab
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus += systemSettingMenu
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus += accountSettingMenu
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs += dashboardTab
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars += issueSidebar
def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders += suggestionProvider
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.toSeq
}
/**
* Provides entry point to PluginRegistry.
*/
object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private val instance = new PluginRegistry()
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).sortBy(_.getName).foreach { pluginJar =>
val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if(databaseVersion != pluginVersion){
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin
))
} catch {
case e: Throwable => {
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
}
}
}
}
}
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
instance.getPlugins().foreach { pluginInfo =>
try {
pluginInfo.pluginClass.shutdown(instance, context, settings)
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)
}
}
}
}
}
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
case class PluginInfo(
pluginId: String,
pluginName: String,
pluginVersion: String,
description: String,
pluginClass: Plugin
)

View File

@@ -1,425 +0,0 @@
package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import java.nio.file.{Files, Paths, StandardWatchEventKinds}
import java.util.Base64
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.apache.commons.io.FileUtils
import org.apache.sshd.server.Command
import org.slf4j.LoggerFactory
import play.twirl.api.Html
import scala.collection.JavaConverters._
class PluginRegistry {
private val plugins = new ConcurrentLinkedQueue[PluginInfo]
private val javaScripts = new ConcurrentLinkedQueue[(String, String)]
private val controllers = new ConcurrentLinkedQueue[(ControllerBase, String)]
private val images = new ConcurrentHashMap[String, String]
private val renderers = new ConcurrentHashMap[String, Renderer]
renderers.put("md", MarkdownRenderer)
renderers.put("markdown", MarkdownRenderer)
private val repositoryRoutings = new ConcurrentLinkedQueue[GitRepositoryRouting]
private val accountHooks = new ConcurrentLinkedQueue[AccountHook]
private val receiveHooks = new ConcurrentLinkedQueue[ReceiveHook]
receiveHooks.add(new ProtectedBranchReceiveHook())
private val repositoryHooks = new ConcurrentLinkedQueue[RepositoryHook]
private val issueHooks = new ConcurrentLinkedQueue[IssueHook]
private val pullRequestHooks = new ConcurrentLinkedQueue[PullRequestHook]
private val repositoryHeaders = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Html]]
private val globalMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val repositoryMenus = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ConcurrentLinkedQueue[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val accountSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val dashboardTabs = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val issueSidebars = new ConcurrentLinkedQueue[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ConcurrentLinkedQueue[(String, String, ClassLoader)]
private val textDecorators = new ConcurrentLinkedQueue[TextDecorator]
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider())
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
def getPlugins(): List[PluginInfo] = plugins.asScala.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = Base64.getEncoder.encodeToString(bytes)
images.put(id, encoded)
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
addImage(id, bytes)
}
def getImage(id: String): String = images.get(id)
def addController(path: String, controller: ControllerBase): Unit = controllers.add((controller, path))
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
def getControllers(): Seq[(ControllerBase, String)] = controllers.asScala.toSeq
def addJavaScript(path: String, script: String): Unit = javaScripts.add((path, script)) //javaScripts += ((path, script))
def getJavaScript(currentPath: String): List[String] = javaScripts.asScala.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def addRenderer(extension: String, renderer: Renderer): Unit = renderers.put(extension, renderer)
def getRenderer(extension: String): Renderer = renderers.asScala.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.asScala.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings.add(routing)
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.asScala.toSeq
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
case GitRepositoryRouting(urlPath, _, _) => {
repositoryPath.matches("/" + urlPath + "(/.*)?")
}
}
}
def addAccountHook(accountHook: AccountHook): Unit = accountHooks.add(accountHook)
def getAccountHooks: Seq[AccountHook] = accountHooks.asScala.toSeq
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks.add(commitHook)
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.asScala.toSeq
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks.add(repositoryHook)
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.asScala.toSeq
def addIssueHook(issueHook: IssueHook): Unit = issueHooks.add(issueHook)
def getIssueHooks: Seq[IssueHook] = issueHooks.asScala.toSeq
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks.add(pullRequestHook)
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.asScala.toSeq
def addRepositoryHeader(repositoryHeader: (RepositoryInfo, Context) => Option[Html]): Unit = repositoryHeaders.add(repositoryHeader)
def getRepositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = repositoryHeaders.asScala.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus.add(globalMenu)
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.asScala.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus.add(repositoryMenu)
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.asScala.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs.add(repositorySettingTab)
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.asScala.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs.add(profileTab)
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.asScala.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus.add(systemSettingMenu)
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.asScala.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus.add(accountSettingMenu)
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.asScala.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs.add(dashboardTab)
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.asScala.toSeq
def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars.add(issueSidebar)
def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.asScala.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings.add(assetsMapping)
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.asScala.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators.add(textDecorator)
def getTextDecorators: Seq[TextDecorator] = textDecorators.asScala.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders.add(suggestionProvider)
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
def addSshCommandProvider(sshCommandProvider: PartialFunction[String, Command]): Unit = sshCommandProviders.add(sshCommandProvider)
def getSshCommandProviders: Seq[PartialFunction[String, Command]] = sshCommandProviders.asScala.toSeq
}
/**
* Provides entry point to PluginRegistry.
*/
object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private var instance = new PluginRegistry()
private var watcher: PluginWatchThread = null
private var extraWatcher: PluginWatchThread = null
private val initializing = new AtomicBoolean(false)
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Reload all plugins.
*/
def reload(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
instance = new PluginRegistry()
initialize(context, settings, conn)
}
/**
* Uninstall a specified plugin.
*/
def uninstall(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
instance.getPlugins()
.collect { case plugin if plugin.pluginId == pluginId => plugin }
.foreach { plugin =>
// try {
// plugin.pluginClass.uninstall(instance, context, settings)
// } catch {
// case e: Exception =>
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
// }
shutdown(context, settings)
plugin.pluginJar.delete()
instance = new PluginRegistry()
initialize(context, settings, conn)
}
}
/**
* Install a plugin from a specified jar file.
*/
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
FileUtils.copyFile(file, new File(PluginHome, file.getName))
instance = new PluginRegistry()
initialize(context, settings, conn)
}
private def listPluginJars(dir: File): Seq[File] = {
dir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).toSeq.sortBy(_.getName).reverse
}
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
// Clean installed directory
val installedDir = new File(PluginHome, ".installed")
if(installedDir.exists){
FileUtils.deleteDirectory(installedDir)
}
installedDir.mkdirs()
val pluginJars = listPluginJars(pluginDir)
val extraJars = extraPluginDir.map { extraDir => listPluginJars(new File(extraDir)) }.getOrElse(Nil)
(extraJars ++ pluginJars).foreach { pluginJar =>
val installedJar = new File(installedDir, pluginJar.getName)
FileUtils.copyFile(pluginJar, installedJar)
logger.info(s"Initialize ${pluginJar.getName}")
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
val pluginId = plugin.pluginId
// Check duplication
instance.getPlugins().find(_.pluginId == pluginId) match {
case Some(x) => {
logger.warn(s"Plugin ${pluginId} is duplicated. ${x.pluginJar.getName} is available.")
}
case None => {
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check database version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if (databaseVersion != pluginVersion) {
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin,
pluginJar = pluginJar,
classLoader = classLoader
))
}
}
} catch {
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
}
}
if(watcher == null){
watcher = new PluginWatchThread(context, PluginHome)
watcher.start()
}
extraPluginDir.foreach { extraDir =>
if(extraWatcher == null){
extraWatcher = new PluginWatchThread(context, extraDir)
extraWatcher.start()
}
}
}
def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized {
instance.getPlugins().foreach { plugin =>
try {
plugin.pluginClass.shutdown(instance, context, settings)
if(watcher != null){
watcher.interrupt()
watcher = null
}
if(extraWatcher != null){
extraWatcher.interrupt()
extraWatcher = null
}
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown: ${plugin.pluginJar.getName}", e)
}
} finally {
plugin.classLoader.close()
}
}
}
}
case class Link(
id: String,
label: String,
path: String,
icon: Option[String] = None
)
class PluginInfoBase(
val pluginId: String,
val pluginName: String,
val pluginVersion: String,
val description: String
)
case class PluginInfo(
override val pluginId: String,
override val pluginName: String,
override val pluginVersion: String,
override val description: String,
pluginClass: Plugin,
pluginJar: File,
classLoader: URLClassLoader
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
import gitbucket.core.model.Profile.profile.blockingApi._
import scala.collection.JavaConverters._
private val logger = LoggerFactory.getLogger(classOf[PluginWatchThread])
override def run(): Unit = {
val path = Paths.get(dir)
if(!Files.exists(path)){
Files.createDirectories(path)
}
val fs = path.getFileSystem
val watcher = fs.newWatchService
val watchKey = path.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.OVERFLOW)
logger.info("Start PluginWatchThread: " + path)
try {
while (watchKey.isValid()) {
val detectedWatchKey = watcher.take()
val events = detectedWatchKey.pollEvents.asScala.filter { e =>
e.context.toString != ".installed" && !e.context.toString.endsWith(".bak")
}
if(events.nonEmpty){
events.foreach { event =>
logger.info(event.kind + ": " + event.context)
}
new Thread {
override def run(): Unit = {
gitbucket.core.servlet.Database() withTransaction { session =>
logger.info("Reloading plugins...")
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
logger.info("Reloading finished.")
}
}
}.start()
}
detectedWatchKey.reset()
}
} catch {
case _: InterruptedException => watchKey.cancel()
}
logger.info("Shutdown PluginWatchThread")
}
}

View File

@@ -1,41 +0,0 @@
package gitbucket.core.plugin
import org.json4s._
import gitbucket.core.util.Directory._
import org.apache.commons.io.FileUtils
object PluginRepository {
implicit val formats = DefaultFormats
def parsePluginJson(json: String): Seq[PluginMetadata] = {
org.json4s.jackson.JsonMethods.parse(json).extract[Seq[PluginMetadata]]
}
lazy val LocalRepositoryDir = new java.io.File(PluginHome, ".repository")
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
def getPlugins(): Seq[PluginMetadata] = {
if(LocalRepositoryIndexFile.exists){
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8"))
} else Nil
}
}
// Mapped from plugins.json
case class PluginMetadata(
id: String,
name: String,
description: String,
versions: Seq[VersionDef],
default: Boolean = false
){
lazy val latestVersion: VersionDef = versions.last
}
case class VersionDef(
version: String,
file: String,
range: String
)

View File

@@ -3,7 +3,6 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService import gitbucket.core.service.RepositoryService
import gitbucket.core.view.Markdown import gitbucket.core.view.Markdown
import gitbucket.core.view.helpers.urlLink
import play.twirl.api.Html import play.twirl.api.Html
/** /**
@@ -34,7 +33,12 @@ object MarkdownRenderer extends Renderer {
object DefaultRenderer extends Renderer { object DefaultRenderer extends Renderer {
override def render(request: RenderRequest): Html = { override def render(request: RenderRequest): Html = {
Html(s"""<tt><pre class="plain">${urlLink(request.fileContent)}</pre></tt>""") import request._
Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
} }
} }
@@ -47,4 +51,4 @@ case class RenderRequest(
enableRefsLink: Boolean, enableRefsLink: Boolean,
enableAnchor: Boolean, enableAnchor: Boolean,
context: Context context: Context
) )

View File

@@ -3,92 +3,15 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
/**
* The base trait of suggestion providers which supplies completion proposals in some text areas.
*/
trait SuggestionProvider { trait SuggestionProvider {
/**
* The identifier of this suggestion provider.
* You must specify the unique identifier in the all suggestion providers.
*/
val id: String 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 val prefix: String
/**
* The suffix of the replaced string. The default is `" "`.
*/
val suffix: String = " " val suffix: String = " "
/**
* Which contexts is this suggestion provider enabled. Currently, available contexts are `"issues"` and `"wiki"`.
*/
val context: Seq[String] val context: Seq[String]
/** def values(repository: RepositoryInfo): Seq[String]
* If this suggestion provider has static proposal list, override this method to return it. def template(implicit context: Context): String = "value"
*
* The returned sequence is rendered as follows:
* <pre>
* [
* {
* "label" -> "value1",
* "value" -> "value1"
* },
* {
* "label" -> "value2",
* "value" -> "value2"
* },
* ]
* </pre>
*
* 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:
* <pre>
* [
* {
* "label" -> "label1",
* "value" -> "value1"
* },
* {
* "label" -> "label2",
* "value" -> "value2"
* },
* ]
* </pre>
*
* 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 = "" def additionalScript(implicit context: Context): String = ""
} }
@@ -97,6 +20,8 @@ class UserNameSuggestionProvider extends SuggestionProvider {
override val id: String = "user" override val id: String = "user"
override val prefix: String = "@" override val prefix: String = "@"
override val context: Seq[String] = Seq("issues") 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 = override def additionalScript(implicit context: Context): String =
s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });""" s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });"""
} }

View File

@@ -50,7 +50,7 @@ trait HandleCommentService {
id id
} }
actionActivity.foreach { f => f(owner, name, userName, issue.issueId, issue.title) } actionActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
// call web hooks // call web hooks
action match { action match {

View File

@@ -32,11 +32,8 @@ trait IssuesService {
.list .list
def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = { def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = {
IssueComments.filter(_.byIssue(owner, repository, issueId)) getCommentsForApi(owner, repository, issueId)
.filter(_.action === "merge".bind) .collectFirst { case (comment, account, _) if comment.action == "merged" => (comment, account) }
.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] = { def getComment(owner: String, repository: String, commentId: String)(implicit s: Session): Option[IssueComment] = {
@@ -202,16 +199,15 @@ trait IssuesService {
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) * @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/ */
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*) def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = { (implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos) searchIssueQueryBase(condition, true, offset, limit, repos)
.join (PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } .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(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 => t5.userName === t1.openedUserName }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName } .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 => i asc }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc } .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => (t1, t5, t2.commentCount, t3, t4, t6) }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list .list
} }
@@ -331,7 +327,6 @@ trait IssuesService {
def createComment(owner: String, repository: String, loginUser: String, def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String)(implicit s: Session): Int = { 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( IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
@@ -347,40 +342,31 @@ trait IssuesService {
Issues Issues
.filter (_.byPrimaryKey(owner, repository, issueId)) .filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => (t.title, t.content.?, t.updatedDate) } .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 = { def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String])(implicit s: Session): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.assignedUserName?, t.updatedDate)).update(assignedUserName, currentDate) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
} }
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = { def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = {
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.milestoneId?, t.updatedDate)).update(milestoneId, currentDate) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
} }
def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = { 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) Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.priorityId?).update (priorityId)
} }
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = { def updateComment(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)
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
} }
def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = { def deleteComment(commentId: Int)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) IssueComments filter (_.byPrimaryKey(commentId)) delete
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 = { 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))
} }
/** /**
@@ -463,8 +449,9 @@ trait IssuesService {
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = { def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = {
extractIssueId(commit.fullMessage).foreach { issueId => extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){ if(getIssue(owner, repository, issueId).isDefined){
val userName = getAccountByMailAddress(commit.committerEmailAddress).map(_.userName).getOrElse(commit.committerName) getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
} }
} }
} }
@@ -510,14 +497,14 @@ object IssuesService {
).flatten ++ ).flatten ++
labels.map(label => s"label:${label}") ++ labels.map(label => s"label:${label}") ++
List( List(
milestone.map { milestone.map { _ match {
case Some(x) => s"milestone:${x}" case Some(x) => s"milestone:${x}"
case None => "no:milestone" case None => "no:milestone"
}, }},
priority.map { priority.map { _ match {
case Some(x) => s"priority:${x}" case Some(x) => s"priority:${x}"
case None => "no:priority" case None => "no:priority"
}, }},
(sort, direction) match { (sort, direction) match {
case ("created" , "desc") => None case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc") case ("created" , "asc" ) => Some("sort:created-asc")

View File

@@ -163,7 +163,7 @@ object MergeService{
case e: NoMergeBaseException => true case e: NoMergeBaseException => true
} }
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip )) val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent val committer = mergeTipCommit.getCommitterIdent;
def updateBranch(treeId:ObjectId, message:String, branchName:String){ def updateBranch(treeId:ObjectId, message:String, branchName:String){
// creates merge commit // creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message) val mergeCommitId = createMergeCommit(treeId, committer, message)

View File

@@ -1,10 +1,11 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.{Session => _, _} import gitbucket.core.model.{ProtectedBranch, ProtectedBranchContext, CommitState}
import gitbucket.core.plugin.ReceiveHook import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
trait ProtectedBranchService { trait ProtectedBranchService {
@@ -17,11 +18,10 @@ trait ProtectedBranchService {
.filter(_._1.byPrimaryKey(owner, repository, branch)) .filter(_._1.byPrimaryKey(owner, repository, branch))
.list .list
.groupBy(_._1) .groupBy(_._1)
.headOption
.map { p => p._1 -> p._2.flatMap(_._2) } .map { p => p._1 -> p._2.flatMap(_._2) }
.map { case (t1, contexts) => .map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin) new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
} }.headOption
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo = def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository)) getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
@@ -45,17 +45,12 @@ trait ProtectedBranchService {
object ProtectedBranchService { object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService with RepositoryService with AccountService { class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String) override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = { (implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/") val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){ if(branch != command.getRefName){
val repositoryInfo = getRepository(owner, repository) getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
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 { } else {
None None
} }
@@ -78,19 +73,10 @@ object ProtectedBranchService {
* Include administrators * Include administrators
* Enforce required status checks for repository administrators. * Enforce required status checks for repository administrators.
*/ */
includeAdministrators: Boolean) extends AccountService with RepositoryService with CommitStatusService { includeAdministrators: Boolean) extends AccountService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean = 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 * Can't be force pushed

View File

@@ -94,9 +94,9 @@ trait PullRequestService { self: IssuesService with CommitsService =>
/** /**
* for repository viewer. * for repository viewer.
* 1. find pull request from `branch` to other branch on same repository * 1. find pull request from from `branch` to othre branch on same repository
* 1. return if exists pull request to `defaultBranch` * 1. return if exists pull request to `defaultBranch`
* 2. return if exists pull request to other branch * 2. return if exists pull request to othre branch
* 2. return None * 2. return None
*/ */
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String) def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
} }
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true, false) val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs) (commits, diffs)
} }
@@ -256,7 +256,7 @@ object PullRequestService {
val statuses: List[CommitStatus] = val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _)) 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 hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = { lazy val commitStateSummary:(CommitState, String) = {

View File

@@ -67,7 +67,7 @@ trait RepositorySearchService { self: IssuesService =>
files.map { case (path, text) => files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query) val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult( FileSearchResult(
path.stripSuffix(".md"), path.replaceFirst("\\.md$", ""),
commits(path).getCommitterIdent.getWhen, commits(path).getCommitterIdent.getWhen,
highlightText, highlightText,
lineNumber) lineNumber)

View File

@@ -135,7 +135,7 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName repositoryName = newRepositoryName
)) :_*) )) :_*)
// TODO Drop transferred owner from collaborators? // TODO Drop transfered owner from collaborators?
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages // Update activity messages
@@ -413,15 +413,6 @@ trait RepositoryService { self: AccountService =>
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1) 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 = { def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {

View File

@@ -54,7 +54,6 @@ trait SystemSettingsService {
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
} }
} }
props.setProperty(SkinName, settings.skinName.toString)
using(new java.io.FileOutputStream(GitBucketConf)){ out => using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null) props.store(out, null)
} }
@@ -112,8 +111,7 @@ trait SystemSettingsService {
getOptionValue(props, LdapKeystore, None))) getOptionValue(props, LdapKeystore, None)))
} else { } else {
None None
}, }
getValue(props, SkinName, "skin-blue")
) )
} }
} }
@@ -138,18 +136,18 @@ object SystemSettingsService {
useSMTP: Boolean, useSMTP: Boolean,
smtp: Option[Smtp], smtp: Option[Smtp],
ldapAuthentication: Boolean, ldapAuthentication: Boolean,
ldap: Option[Ldap], ldap: Option[Ldap]){
skinName: String){ def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
def baseUrl(request: HttpServletRequest): String = baseUrl.fold { def sshAddress:Option[SshAddress] =
val url = request.getRequestURL.toString for {
val len = url.length - (request.getRequestURI.length - request.getContextPath.length) host <- sshHost if ssh
url.substring(0, len).stripSuffix("/") }
} (_.stripSuffix("/")) yield SshAddress(
host,
def sshAddress:Option[SshAddress] = sshHost.collect { case host if ssh => sshPort.getOrElse(DefaultSshPort),
SshAddress(host, sshPort.getOrElse(DefaultSshPort), "git") "git"
} )
} }
case class Ldap( case class Ldap(
@@ -221,7 +219,6 @@ object SystemSettingsService {
private val LdapTls = "ldap.tls" private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl" private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore" private val LdapKeystore = "ldap.keystore"
private val SkinName = "skinName"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse { getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse {

View File

@@ -228,18 +228,16 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){ callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{ for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) (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) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestPayload( WebHookPullRequestPayload(
action = action, action = action,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = headRepo, headRepository = headRepo,
headOwner = headOwner, headOwner = headOwner,
@@ -275,14 +273,12 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._ import WebHookService._
for{ for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch) ((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) baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield { } yield {
val payload = WebHookPullRequestPayload( val payload = WebHookPullRequestPayload(
action = action, action = action,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = requestRepository, headRepository = requestRepository,
headOwner = headOwner, headOwner = headOwner,
@@ -300,17 +296,16 @@ trait WebHookPullRequestService extends WebHookService {
trait WebHookPullRequestReviewCommentService extends WebHookService { trait WebHookPullRequestReviewCommentService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService => self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
issue: Issue, pullRequest: PullRequest, baseUrl: String, sender: Account)
(implicit s: Session, c: JsonFormat.Context): Unit = { (implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._ import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){ callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
val users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for{ 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) baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName) headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName) issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield { } yield {
WebHookPullRequestReviewCommentPayload( WebHookPullRequestReviewCommentPayload(
@@ -318,7 +313,6 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment, comment = comment,
issue = issue, issue = issue,
issueUser = issueUser, issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest, pullRequest = pullRequest,
headRepository = headRepo, headRepository = headRepo,
headOwner = headOwner, headOwner = headOwner,
@@ -373,9 +367,9 @@ object WebHookService {
repository: ApiRepository repository: ApiRepository
) extends FieldSerializable with WebHookPayload { ) extends FieldSerializable with WebHookPayload {
val compare = commits.size match { val compare = commits.size match {
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initialized repository case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}") case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
case _ if before.forall(_=='0') => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}") case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}") case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
} }
val head_commit = commits.lastOption val head_commit = commits.lastOption
@@ -430,7 +424,6 @@ object WebHookService {
def apply(action: String, def apply(action: String,
issue: Issue, issue: Issue,
issueUser: Account, issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest, pullRequest: PullRequest,
headRepository: RepositoryInfo, headRepository: RepositoryInfo,
headOwner: Account, headOwner: Account,
@@ -448,7 +441,6 @@ object WebHookService {
headRepo = headRepoPayload, headRepo = headRepoPayload,
baseRepo = baseRepoPayload, baseRepo = baseRepoPayload,
user = ApiUser(issueUser), user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment mergedComment = mergedComment
) )
@@ -503,7 +495,6 @@ object WebHookService {
comment: CommitComment, comment: CommitComment,
issue: Issue, issue: Issue,
issueUser: Account, issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest, pullRequest: PullRequest,
headRepository: RepositoryInfo, headRepository: RepositoryInfo,
headOwner: Account, headOwner: Account,
@@ -511,7 +502,7 @@ object WebHookService {
baseOwner: Account, baseOwner: Account,
sender: Account, sender: Account,
mergedComment: Option[(IssueComment, Account)] mergedComment: Option[(IssueComment, Account)]
): WebHookPullRequestReviewCommentPayload = { ) : WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner) val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner) val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender) val senderPayload = ApiUser(sender)
@@ -530,7 +521,6 @@ object WebHookService {
headRepo = headRepoPayload, headRepo = headRepoPayload,
baseRepo = baseRepoPayload, baseRepo = baseRepoPayload,
user = ApiUser(issueUser), user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment mergedComment = mergedComment
), ),
repository = baseRepoPayload, repository = baseRepoPayload,

View File

@@ -237,7 +237,7 @@ trait WikiService {
builder.finish() builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress, Constants.HEAD, committer.fullName, committer.mailAddress,
if(message.trim.isEmpty) { if(message.trim.length == 0) {
if(removed){ if(removed){
s"Rename ${currentPageName} to ${newPageName}" s"Rename ${currentPageName} to ${newPageName}"
} else if(created){ } else if(created){

View File

@@ -1,69 +0,0 @@
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 + "/"
}
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)
}

View File

@@ -0,0 +1,37 @@
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.
* <code>/:user/:repo.git/</code>
*/
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() = {}
}

View File

@@ -74,7 +74,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
val action = request.paths match { val action = request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) => case Array(_, repositoryOwner, repositoryName, _*) =>
Database() withSession { implicit session => Database() withSession { implicit session =>
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match { getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
case Some(repository) => { case Some(repository) => {
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) { val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
// Authentication is not required // Authentication is not required

View File

@@ -156,13 +156,9 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository) 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")){ if(!repository.endsWith(".wiki")){
defining(request) { implicit r => defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl) val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
receivePack.setPreReceiveHook(hook) receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook) receivePack.setPostReceiveHook(hook)
} }
@@ -170,7 +166,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
if(repository.endsWith(".wiki")){ if(repository.endsWith(".wiki")){
defining(request) { implicit r => defining(request) { implicit r =>
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl, sshUrl)) receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.replaceFirst("\\.wiki$", ""), pusher, baseUrl))
} }
} }
} }
@@ -182,7 +178,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String]) class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)
extends PostReceiveHook with PreReceiveHook extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService with CommitsService { with WebHookPullRequestService with CommitsService {
@@ -223,7 +219,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]() val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command => commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl) implicit val apiContext = api.JsonFormat.Context(baseUrl)
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/") val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") { val commits = if (refName(1) == "tags") {
@@ -324,7 +320,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String]) class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String)
extends PostReceiveHook with WebHookService with AccountService with RepositoryService { extends PostReceiveHook with WebHookService with AccountService with RepositoryService {
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook]) private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
@@ -333,7 +329,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
Database() withTransaction { implicit session => Database() withTransaction { implicit session =>
try { try {
commands.asScala.headOption.foreach { command => commands.asScala.headOption.foreach { command =>
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl) implicit val apiContext = api.JsonFormat.Context(baseUrl)
val refName = command.getRefName.split("/") val refName = command.getRefName.split("/")
val commitIds = if (refName(1) == "tags") { val commitIds = if (refName(1) == "tags") {
None None
@@ -351,6 +347,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") => diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited" val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
val fileName = diff.newPath val fileName = diff.newPath
println(action + " - " + fileName + " - " + commit.id)
(action, fileName, commit.id) (action, fileName, commit.id)
} }
} }

View File

@@ -1,30 +1,25 @@
package gitbucket.core.servlet package gitbucket.core.servlet
import java.io.{File, FileOutputStream} import java.io.File
import akka.event.Logging import akka.event.Logging
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.{PluginRegistry, PluginRepository} import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.{ActivityService, SystemSettingsService} import gitbucket.core.service.{ActivityService, SystemSettingsService}
import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.JDBCUtil._ import gitbucket.core.util.JDBCUtil._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import javax.servlet.{ServletContextEvent, ServletContextListener} import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.apache.commons.io.FileUtils
import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import akka.actor.{Actor, ActorSystem, Props} import akka.actor.{Actor, Props, ActorSystem}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import com.github.zafarkhaja.semver.{Version => Semver}
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
/** /**
* Initialize GitBucket system. * Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing. * Update database schema and load plug-ins automatically in the context initializing.
@@ -59,11 +54,44 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
val manager = new JDBCVersionManager(conn) val manager = new JDBCVersionManager(conn)
// Check version // Check version
checkVersion(manager, conn) 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.")
}
}
// Run normal migration // Run normal migration
logger.info("Start schema update") logger.info("Start schema update")
new Solidbase().migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule) val solidbase = new Solidbase()
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Rescue code for users who updated from 3.14 to 4.0.0 // Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227 // https://github.com/gitbucket/gitbucket/issues/1227
@@ -78,9 +106,6 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.") throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
} }
// Install bundled plugins
extractBundledPlugins(gitbucketVersion)
// Load plugins // Load plugins
logger.info("Initialize plugins") logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
@@ -92,76 +117,7 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity") 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 = { override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler // Shutdown Quartz scheduler
@@ -190,4 +146,4 @@ class DeleteOldActivityActor extends Actor with SystemSettingsService with Activ
} }
} }
} }
} }

View File

@@ -19,7 +19,7 @@ class PluginAssetsServlet extends HttpServlet {
.find { case (prefix, _, _) => path.startsWith("/plugin-assets" + prefix) } .find { case (prefix, _, _) => path.startsWith("/plugin-assets" + prefix) }
.flatMap { case (prefix, resourcePath, classLoader) => .flatMap { case (prefix, resourcePath, classLoader) =>
val resourceName = path.substring(("/plugin-assets" + prefix).length) val resourceName = path.substring(("/plugin-assets" + prefix).length)
Option(classLoader.getResourceAsStream(resourcePath.stripPrefix("/") + resourceName)) Option(classLoader.getResourceAsStream(resourcePath.replaceFirst("^/", "") + resourceName))
} }
.map { in => .map { in =>
try { try {

View File

@@ -1,47 +0,0 @@
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)
}
}

View File

@@ -23,7 +23,7 @@ class TransactionFilter extends Filter {
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
val servletPath = req.asInstanceOf[HttpServletRequest].getServletPath() val servletPath = req.asInstanceOf[HttpServletRequest].getServletPath()
if(servletPath.startsWith("/assets/") || servletPath == "/console" || servletPath == "/git" || servletPath == "/git-lfs"){ if(servletPath.startsWith("/assets/") || servletPath == "/git" || servletPath == "/git-lfs"){
// assets and git-lfs don't need transaction // assets and git-lfs don't need transaction
chain.doFilter(req, res) chain.doFilter(req, res)
} else { } else {

View File

@@ -19,7 +19,7 @@ import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand { 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 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, sshUrl: Option[String]) extends DefaultGitCommand(owner, repoName) class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService with DeployKeyService { with RepositoryService with AccountService with DeployKeyService {
override protected def runTask(authType: AuthType): Unit = { override protected def runTask(authType: AuthType): Unit = {
@@ -169,7 +169,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, ss
val repository = git.getRepository val repository = git.getRepository
val receive = new ReceivePack(repository) val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) { if (!repoName.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl) val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl)
receive.setPreReceiveHook(hook) receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook) receive.setPostReceiveHook(hook)
} }
@@ -216,26 +216,19 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) exte
} }
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory { class GitCommandFactory(baseUrl: String) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = { override def createCommand(command: String): Command = {
import GitCommand._ import GitCommand._
logger.debug(s"command: $command") logger.debug(s"command: $command")
val pluginCommand = PluginRegistry().getSshCommandProviders.collectFirst { command match {
case f if f.isDefinedAt(command) => f(command) 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)
pluginCommand match { case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case Some(x) => x case _ => new UnknownCommand(command)
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)
}
} }
} }

View File

@@ -6,7 +6,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress 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.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -22,7 +22,7 @@ object SshServer {
provider.setOverwriteAllowed(false) provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider) server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser)) server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setCommandFactory(new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}"))) server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell(sshAddress)) server.setShellFactory(new NoShell(sshAddress))
} }

View File

@@ -92,7 +92,7 @@ object DatabaseType {
} }
object MySQL extends DatabaseType { object MySQL extends DatabaseType {
val jdbcDriver = "org.mariadb.jdbc.Driver" val jdbcDriver = "com.mysql.jdbc.Driver"
val slickDriver = BlockingMySQLDriver val slickDriver = BlockingMySQLDriver
val liquiDriver = new MySQLDatabase() val liquiDriver = new MySQLDatabase()
} }

View File

@@ -90,4 +90,4 @@ object Directory {
def getWikiRepositoryDir(owner: String, repository: String): File = def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
} }

View File

@@ -28,6 +28,8 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") 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 isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0) def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -51,6 +53,16 @@ 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 = def getLfsFilePath(owner: String, repository: String, oid: String): String =
Directory.getLfsDir(owner, repository) + "/" + oid Directory.getLfsDir(owner, repository) + "/" + oid

View File

@@ -22,8 +22,7 @@ object Implicits {
// Convert to slick session. // Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
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 { implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
@@ -78,6 +77,11 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/") 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 { implicit class RichSession(private val session: HttpSession) extends AnyVal {

View File

@@ -75,7 +75,7 @@ object JDBCUtil {
var stringLiteral = false var stringLiteral = false
while({ length = in.read(bytes); length != -1 }){ while({ length = in.read(bytes); length != -1 }){
for(i <- 0 until length){ for(i <- 0 to length - 1){
val c = bytes(i) val c = bytes(i)
if(c == '\''){ if(c == '\''){
stringLiteral = !stringLiteral stringLiteral = !stringLiteral
@@ -146,11 +146,13 @@ object JDBCUtil {
} }
} }
val columnValues = values.map { val columnValues = values.map { value =>
case x: String => "'" + x.replace("'", "''") + "'" value match {
case x: Timestamp => "'" + dateFormat.format(x) + "'" case x: String => "'" + x.replace("'", "''") + "'"
case null => "NULL" case x: Timestamp => "'" + dateFormat.format(x) + "'"
case x => x case null => "NULL"
case x => x
}
} }
sb.append(columnValues.mkString(", ")) sb.append(columnValues.mkString(", "))
sb.append(");\n") sb.append(");\n")

View File

@@ -1,7 +1,5 @@
package gitbucket.core.util package gitbucket.core.util
import java.io.ByteArrayOutputStream
import gitbucket.core.service.RepositoryService import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import Directory._ import Directory._
@@ -16,7 +14,7 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException} import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.transport.RefSpec
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -24,7 +22,6 @@ import java.util.function.Consumer
import org.cache2k.{Cache2kBuilder, CacheEntry} import org.cache2k.{Cache2kBuilder, CacheEntry}
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException} import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -96,7 +93,7 @@ object JGitUtil {
val summary = getSummaryMessage(fullMessage, shortMessage) val summary = getSummaryMessage(fullMessage, shortMessage)
val description = defining(fullMessage.trim.indexOf('\n')){ i => val description = defining(fullMessage.trim.indexOf("\n")){ i =>
if(i >= 0){ if(i >= 0){
Some(fullMessage.trim.substring(i).trim) Some(fullMessage.trim.substring(i).trim)
} else None } else None
@@ -117,8 +114,7 @@ object JGitUtil {
newObjectId: Option[String], newObjectId: Option[String],
oldMode: String, oldMode: String,
newMode: String, newMode: String,
tooLarge: Boolean, tooLarge: Boolean
patch: Option[String]
) )
/** /**
@@ -230,14 +226,9 @@ object JGitUtil {
ref.getName.stripPrefix("refs/heads/") ref.getName.stripPrefix("refs/heads/")
}.toList, }.toList,
// tags // tags
git.tagList.call.asScala.flatMap { ref => git.tagList.call.asScala.map { ref =>
try { val revCommit = getRevCommitFromId(git, ref.getObjectId)
val revCommit = getRevCommitFromId(git, ref.getObjectId) TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
Some(TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName))
} catch {
case _: IncorrectObjectTypeException =>
None
}
}.sortBy(_.time).toList }.sortBy(_.time).toList
) )
} catch { } catch {
@@ -297,7 +288,7 @@ object JGitUtil {
@tailrec @tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)], def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, 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){ if(restList.isEmpty){
result result
} else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty } else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
@@ -368,9 +359,9 @@ object JGitUtil {
(file1.isDirectory, file2.isDirectory) match { (file1.isDirectory, file2.isDirectory) match {
case (true , false) => true case (true , false) => true
case (false, true ) => false case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0 case _ => file1.name.compareTo(file2.name) < 0
} }
} }.toList
} }
} }
@@ -378,7 +369,7 @@ object JGitUtil {
* Returns the first line of the commit message. * Returns the first line of the commit message.
*/ */
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = { 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 => defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine if(firstLine.length > shortMessage.length) shortMessage else firstLine
} }
@@ -519,10 +510,9 @@ object JGitUtil {
} }
/** /**
* Returns the tuple of diff of the given commit and parent commit ids. * Returns the tuple of diff of the given commit and the previous commit id.
* DiffInfos returned from this method don't include the patch property.
*/ */
def getDiffs(git: Git, id: String, fetchContent: Boolean): (List[DiffInfo], Option[String]) = { def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec @scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match { i.hasNext match {
@@ -543,7 +533,7 @@ object JGitUtil {
} else { } else {
commits(1) commits(1)
} }
(getDiffs(git, oldCommit.getName, id, fetchContent, false), Some(oldCommit.getName)) (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
} else { } else {
// initial commit // initial commit
@@ -556,7 +546,7 @@ object JGitUtil {
buffer.append((if(!fetchContent){ buffer.append((if(!fetchContent){
DiffInfo( DiffInfo(
changeType = ChangeType.ADD, changeType = ChangeType.ADD,
oldPath = "", oldPath = null,
newPath = treeWalk.getPathString, newPath = treeWalk.getPathString,
oldContent = None, oldContent = None,
newContent = None, newContent = None,
@@ -566,13 +556,12 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString, oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString, newMode = treeWalk.getFileMode(0).toString,
tooLarge = false, tooLarge = false
patch = None
) )
} else { } else {
DiffInfo( DiffInfo(
changeType = ChangeType.ADD, changeType = ChangeType.ADD,
oldPath = "", oldPath = null,
newPath = treeWalk.getPathString, newPath = treeWalk.getPathString,
oldContent = None, oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray), newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
@@ -582,8 +571,7 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name), newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString, oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString, newMode = treeWalk.getFileMode(0).toString,
tooLarge = false, tooLarge = false
patch = None
) )
})) }))
} }
@@ -593,7 +581,7 @@ object JGitUtil {
} }
} }
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = { def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
val reader = git.getRepository.newObjectReader val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
@@ -619,8 +607,7 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name), newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString, oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString, newMode = diff.getNewMode.toString,
tooLarge = true, tooLarge = true
patch = None
) )
} else { } else {
val oldIsImage = FileUtil.isImage(diff.getOldPath) val oldIsImage = FileUtil.isImage(diff.getOldPath)
@@ -638,8 +625,7 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name), newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString, oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString, newMode = diff.getNewMode.toString,
tooLarge = false, tooLarge = false
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
) )
} else { } else {
DiffInfo( DiffInfo(
@@ -654,23 +640,13 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name), newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString, oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString, newMode = diff.getNewMode.toString,
tooLarge = false, tooLarge = false
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
) )
} }
} }
}.toList }.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. * Returns the list of branch names of the specified commit.
@@ -1018,13 +994,13 @@ object JGitUtil {
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = { def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId => 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.setStartCommit(commitId)
blamer.setFilePath(path) blamer.setFilePath(path)
val blame = blamer.call() val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]() var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]() 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) val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){ if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo( blameMap += c.name -> JGitUtil.BlameInfo(
@@ -1034,7 +1010,7 @@ object JGitUtil {
c.getAuthorIdent.getWhen, c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next) Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name), .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.getCommitterIdent.getWhen,
c.getShortMessage, c.getShortMessage,
Set.empty) Set.empty)

View File

@@ -1,68 +0,0 @@
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 = ()
//}

View File

@@ -0,0 +1,212 @@
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
/**
* The trait for notifications.
* This is used by notifications plugin, which provides notifications feature on GitBucket.
* Please see the plugin for details.
*/
trait Notifier {
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit
}
object Notifier {
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
// TODO This class is temporary keeping the current feature until Notifications Plugin is available.
class IssueHook extends gitbucket.core.plugin.IssueHook
with RepositoryService with AccountService with IssuesService {
override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(issue.content getOrElse "", r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(content, r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def closed(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("close", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
override def reopened(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("reopen", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
protected def subject(issue: Issue, r: RepositoryService.RepositoryInfo): String =
s"[${r.owner}/${r.name}] ${issue.title} (#${issue.issueId})"
protected def message(content: String, r: RepositoryService.RepositoryInfo)(msg: String => String)(implicit context: Context): String =
msg(Markdown.toHtml(
markdown = content,
repository = r,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = false,
enableLineBreaks = false
))
protected val recipients: Issue => Account => Session => Seq[String] = {
issue => loginAccount => implicit 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
.flatMap (
getAccountByUserName(_)
.filterNot (_.isGroupAccount)
.filterNot (LDAPUtil.isDummyMailAddress)
.map (_.mailAddress)
)
}
}
// TODO This class is temporary keeping the current feature until Notifications Plugin is available.
class PullRequestHook extends IssueHook with gitbucket.core.plugin.PullRequestHook {
override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
val url = s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"
Notifier().toNotify(
subject(issue, r),
message(issue.content getOrElse "", r)(content => s"""
|$content<hr/>
|View, comment on, or merge it at:<br/>
|<a href="$url">$url</a>
""".stripMargin)
)(recipients(issue))
}
override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(content, r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def merged(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("merge", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
}
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = {
context.loginAccount.foreach { loginAccount =>
val database = Database()
val f = Future {
database withSession { session =>
recipients(loginAccount)(session) foreach { to =>
send(to, subject, msg, loginAccount)
}
}
"Notifications Successful."
}
f.onComplete {
case Success(s) => logger.debug(s)
case Failure(t) => logger.error("Notifications Failed.", t)
}
}
}
def send(to: String, subject: String, msg: String, loginAccount: Account): Unit = {
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.userName))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
}
class MockMailer extends Notifier {
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
}

View File

@@ -53,14 +53,4 @@ object SyntaxSugars {
def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t) def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t)
} }
/**
* Provides easier and explicit ways to access to a head value of `Map[String, Seq[String]]`.
* This is intended to use in implementations of scalatra-forms's `Constraint` or `ValueType`.
*/
implicit class HeadValueAccessibleMap(map: Map[String, Seq[String]]){
def value(key: String): String = map(key).head
def optionValue(key: String): Option[String] = map.get(key).flatMap(_.headOption)
def values(key: String): Seq[String] = map.get(key).getOrElse(Seq.empty)
}
} }

View File

@@ -1,6 +1,6 @@
package gitbucket.core.util package gitbucket.core.util
import org.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
trait Validations { trait Validations {
@@ -24,7 +24,7 @@ trait Validations {
*/ */
def password: Constraint = new Constraint(){ def password: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = override def validate(name: String, value: String, messages: Messages): Option[String] =
if(System.getProperty("gitbucket.validate.password") != "false" && !value.matches("[a-zA-Z0-9\\-_.]+")){ if(!value.matches("[a-zA-Z0-9\\-_.]+")){
Some(s"${name} contains invalid character.") Some(s"${name} contains invalid character.")
} else { } else {
None None

View File

@@ -128,7 +128,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
repository: RepositoryService.RepositoryInfo, repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = { enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
val fileName = filePath.last.toLowerCase val fileName = filePath.reverse.head.toLowerCase
val extension = FileUtil.getExtension(fileName) val extension = FileUtil.getExtension(fileName)
val renderer = PluginRegistry().getRenderer(extension) val renderer = PluginRegistry().getRenderer(extension)
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context)) renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))
@@ -346,10 +346,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
} }
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string) // This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val urlRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def urlLink(text: String): String = { def detectAndRenderLinks(text: String, repository: RepositoryInfo)(implicit context: Context): String = {
val matches = urlRegex.findAllMatchIn(text).toSeq val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) => val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
val url = m.group(0) val url = m.group(0)
@@ -361,7 +361,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
} }
// append rest fragment // append rest fragment
val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x
HtmlFormat.fill(out).toString
decorateHtml(HtmlFormat.fill(out).toString, repository)
} }
/** /**

View File

@@ -56,6 +56,7 @@
<a href="@context.path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a> <a href="@context.path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div> </div>
<input type="submit" class="btn btn-success" value="Save"/> <input type="submit" class="btn btn-success" value="Save"/>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@helpers.url(account.userName)" class="btn btn-default">Cancel</a>}
</div> </div>
</form> </form>
} }

View File

@@ -35,7 +35,7 @@
@menu(context).map { link => @menu(context).map { link =>
<li@if(active==link.id){ class="active"}> <li@if(active==link.id){ class="active"}>
<a href="@context.path/@link.path"> <a href="@context.path/@link.path">
<i class="menu-icon octicon octicon-@link.icon.getOrElse("plug")"></i> <i class="menu-icon octicon octicon-plug"></i>
<span>@link.label</span> <span>@link.label</span>
</a> </a>
</li> </li>

View File

@@ -1,32 +1,18 @@
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context) @(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Plugins"){ @gitbucket.core.html.main("Plugins"){
@gitbucket.core.admin.html.menu("plugins") { @gitbucket.core.admin.html.menu("plugins") {
@gitbucket.core.helper.html.information(info) <h1>Installed plugins</h1>
<form action="@context.path/admin/plugins/_reload" method="POST" class="pull-right">
<input type="submit" value="Reload plugins" class="btn btn-default">
</form>
<h1>Plugins</h1>
@if(plugins.size > 0) { @if(plugins.size > 0) {
<ul> <ul>
@plugins.map { case (plugin, enabled) => @plugins.map { plugin =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li> <li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
} }
</ul> </ul>
@plugins.map { case (plugin, enabled) => @plugins.map { plugin =>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong" id="@plugin.pluginId"> <div class="panel-heading strong" id="@plugin.pluginId">@plugin.pluginName</div>
@if(enabled){
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
} else {
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
}
@plugin.pluginName
</div>
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<label class="col-md-2">Id</label> <label class="col-md-2">Id</label>
@@ -52,16 +38,3 @@
} }
} }
} }
<script>
$(function(){
$('.uninstall-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Uninstall ' + name + '. Are you sure?');
});
$('.install-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Install ' + name + '. Are you sure?');
});
});
</script>

View File

@@ -1,6 +1,5 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context) @(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig @import gitbucket.core.util.DatabaseConfig
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("System settings"){ @gitbucket.core.html.main("System settings"){
@gitbucket.core.admin.html.menu("system"){ @gitbucket.core.admin.html.menu("system"){
@gitbucket.core.helper.html.information(info) @gitbucket.core.helper.html.information(info)
@@ -60,44 +59,6 @@
<textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea> <textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- AdminLTE SkinName -->
<!--====================================================================-->
<hr>
<label class="strong">
AdminLTE skin name
</label>
<div class="form-group">
<label class="control-label col-md-2" for="skinName">Skin name</label>
<div class="col-md-10">
<select id="skinName" name="skinName" class="form-control">
<optgroup label="Dark">
@Seq(
("skin-black", "Black"),
("skin-blue", "Blue"),
("skin-green", "Green"),
("skin-purple", "Purple"),
("skin-red", "Red"),
("skin-yellow", "Yellow"),
).map{ skin =>
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""}>@skin._2</option>
}
</optgroup>
<optgroup label="Light">
@Seq(
("skin-black-light", "Light black"),
("skin-blue-light", "Light blue"),
("skin-green-light", "Light green"),
("skin-purple-light", "Light purple"),
("skin-red-light", "Light red"),
("skin-yellow-light", "Light yellow"),
).map{ skin =>
<option value="@skin._1"@if(skin._1 == context.settings.skinName){ selected=""} >@skin._2</option>
}
</optgroup>
</select>
</div>
</div>
<!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
@@ -146,8 +107,8 @@
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label> <label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="activityLogLimit">Limit</label> <label class="control-label col-md-3" for="activityLogLimit">Limit</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/> <input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span> <span id="error-activityLogLimit" class="error"></span>
</div> </div>
@@ -178,15 +139,15 @@
</fieldset> </fieldset>
<div class="ssh"> <div class="ssh">
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="sshHost">SSH host</label> <label class="control-label col-md-3" for="sshHost">SSH host</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/> <input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/>
<span id="error-sshHost" class="error"></span> <span id="error-sshHost" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="sshPort">SSH port</label> <label class="control-label col-md-3" for="sshPort">SSH port</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/> <input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/>
<span id="error-sshPort" class="error"></span> <span id="error-sshPort" class="error"></span>
</div> </div>
@@ -205,83 +166,83 @@
</fieldset> </fieldset>
<div class="ldap"> <div class="ldap">
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapHost">LDAP host</label> <label class="control-label col-md-3" for="ldapHost">LDAP host</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/> <input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span> <span id="error-ldap_host" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapPort">LDAP port</label> <label class="control-label col-md-3" for="ldapPort">LDAP port</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/> <input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span> <span id="error-ldap_port" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapBindDN">Bind DN</label> <label class="control-label col-md-3" for="ldapBindDN">Bind DN</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/> <input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span> <span id="error-ldap_bindDN" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapBindPassword">Bind password</label> <label class="control-label col-md-3" for="ldapBindPassword">Bind password</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/> <input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span> <span id="error-ldap_bindPassword" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapBaseDN">Base DN</label> <label class="control-label col-md-3" for="ldapBaseDN">Base DN</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/> <input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span> <span id="error-ldap_baseDN" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapUserNameAttribute">User name attribute</label> <label class="control-label col-md-3" for="ldapUserNameAttribute">User name attribute</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/> <input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span> <span id="error-ldap_userNameAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapAdditionalFilterCondition">Additional filter condition</label> <label class="control-label col-md-3" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/> <input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span> <span id="error-ldap_additionalFilterCondition" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapFullNameAttribute">Full name attribute</label> <label class="control-label col-md-3" for="ldapFullNameAttribute">Full name attribute</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/> <input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span> <span id="error-ldap_fullNameAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapMailAttribute">Mail address attribute</label> <label class="control-label col-md-3" for="ldapMailAttribute">Mail address attribute</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/> <input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span> <span id="error-ldap_mailAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2">Enable TLS</label> <label class="control-label col-md-3">Enable TLS</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> <input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2">Enable SSL</label> <label class="control-label col-md-3">Enable SSL</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/> <input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="ldapBindDN">Keystore</label> <label class="control-label col-md-3" for="ldapBindDN">Keystore</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/> <input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span> <span id="error-ldap_keystore" class="error"></span>
</div> </div>
@@ -312,52 +273,52 @@
</fieldset> </fieldset>
<div class="useSMTP"> <div class="useSMTP">
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpHost">SMTP host</label> <label class="control-label col-md-3" for="smtpHost">SMTP host</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/> <input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span> <span id="error-smtp_host" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpPort">SMTP port</label> <label class="control-label col-md-3" for="smtpPort">SMTP port</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/> <input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span> <span id="error-smtp_port" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpUser">SMTP user</label> <label class="control-label col-md-3" for="smtpUser">SMTP user</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/> <input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpPassword">SMTP password</label> <label class="control-label col-md-3" for="smtpPassword">SMTP password</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/> <input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpSsl">Enable SSL</label> <label class="control-label col-md-3" for="smtpSsl">Enable SSL</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> <input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpStarttls">Enable STARTTLS</label> <label class="control-label col-md-3" for="smtpStarttls">Enable STARTTLS</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/> <input type="checkbox" id="smtpStarttls" name="smtp.starttls"@if(context.settings.smtp.flatMap(_.starttls).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="fromAddress">FROM address</label> <label class="control-label col-md-3" for="fromAddress">FROM address</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/> <input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="fromName">FROM name</label> <label class="control-label col-md-3" for="fromName">FROM name</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/> <input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@context.settings.smtp.map(_.fromName)"/>
</div> </div>
</div> </div>
@@ -367,17 +328,17 @@
<input type="button" id="sendTestMail" value="Send"/> <input type="button" id="sendTestMail" value="Send"/>
</div> </div>
</div> </div>
@*
<!--====================================================================--> <!--====================================================================-->
<!-- GitLFS --> <!-- GitLFS -->
<!--====================================================================--> <!--====================================================================-->
@*
<hr> <hr>
<label class="strong"> <label class="strong">
GitLFS <span class="muted normal">(Enter the LFS server url to enable GitLFS support)</span> GitLFS <span class="muted normal">(Enter the LFS server url to enable GitLFS support)</span>
</label> </label>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-2" for="smtpHost">LFS server url</label> <label class="control-label col-md-3" for="smtpHost">LFS server url</label>
<div class="col-md-10"> <div class="col-md-9">
<input type="text" id="lfsServerUrl" name="lfs.serverUrl" class="form-control" value="@context.settings.lfs.serverUrl"/> <input type="text" id="lfsServerUrl" name="lfs.serverUrl" class="form-control" value="@context.settings.lfs.serverUrl"/>
<span id="error-lfs_serverUrl" class="error"></span> <span id="error-lfs_serverUrl" class="error"></span>
</div> </div>
@@ -393,14 +354,6 @@
} }
<script> <script>
$(function(){ $(function(){
$('#skinName').change(function(evt) {
var that = $(evt.target);
var themeCss = $('link[rel="stylesheet"][href*="skin-"]');
var oldVal = new RegExp('(skin-.*?).min.css').exec(themeCss.attr('href'))[1];
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
$(document.body).removeClass(oldVal).addClass(that.val());
});
$('#sendTestMail').click(function(){ $('#sendTestMail').click(function(){
var host = $('#smtpHost' ).val(); var host = $('#smtpHost' ).val();
var port = $('#smtpPort' ).val(); var port = $('#smtpPort' ).val();

View File

@@ -14,7 +14,7 @@
} else { } else {
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li> <li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
@userRepositories.zipWithIndex.map { case (repository, i) => @userRepositories.zipWithIndex.map { case (repository, i) =>
<li class="repo-link menu-item-hover"> <li class="menu-item-hover">
@if(repository.owner == context.loginAccount.get.userName){ @if(repository.owner == context.loginAccount.get.userName){
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span class="strong">@repository.name</span></a> <a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span class="strong">@repository.name</span></a>
} else { } else {
@@ -30,7 +30,7 @@
} else { } else {
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li> <li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
@recentRepositories.zipWithIndex.map { case (repository, i) => @recentRepositories.zipWithIndex.map { case (repository, i) =>
<li class="repo-link menu-item-hover"> <li class="menu-item-hover">
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span>@repository.owner/<span class="strong">@repository.name</span></span></a> <a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span>@repository.owner/<span class="strong">@repository.name</span></span></a>
</li> </li>
} }

View File

@@ -1,22 +1,8 @@
@(title: String, e: Option[Throwable]=None)(implicit context: gitbucket.core.controller.Context) @(title: String)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Error"){ @gitbucket.core.html.main("Error"){
<div class="content-wrapper main-center"> <div class="content-wrapper main-center">
<div class="content body"> <div class="content body">
<h1>@title</h1> <h1>@title</h1>
@if(context.loginAccount.map{_.isAdmin}.getOrElse(false)){
@e.map { ex =>
<h2>@ex.toString</h2>
<table class="table table-condensed table-striped table-hover">
<tbody>
@ex.getStackTrace.map{ st =>
<tr><td>@st</td></tr>
}
</tbody>
</table>
}
} else {
<div>Please contact your administrator.</div>
}
</div> </div>
</div> </div>
} }

View File

@@ -17,12 +17,6 @@ $(function(){
function (data) { function (data) {
return process(data.options); return process(data.options);
}); });
},
displayText: function(item) {
return item.label;
},
afterSelect: function(item) {
$('#@id').val(item.value);
} }
}); });
}); });

View File

@@ -11,9 +11,7 @@
$(function(){ $(function(){
@gitbucket.core.plugin.PluginRegistry().getSuggestionProviders.map { provider => @gitbucket.core.plugin.PluginRegistry().getSuggestionProviders.map { provider =>
@if(provider.context.contains(completionContext)){ @if(provider.context.contains(completionContext)){
var @provider.id = @Html(helpers.json(provider.options(repository).map { case (value, label) => var @provider.id = @Html(helpers.json(provider.values(repository)));
Map("value" -> value, "label" -> label)
}));
@Html(provider.additionalScript) @Html(provider.additionalScript)
} }
} }
@@ -25,14 +23,14 @@ $(function(){
match: /\B@{provider.prefix}([\-+\w]*)$/, match: /\B@{provider.prefix}([\-+\w]*)$/,
search: function (term, callback) { search: function (term, callback) {
callback($.map(@{provider.id}, function (proposal) { callback($.map(@{provider.id}, function (proposal) {
return proposal.value.indexOf(term) === 0 ? proposal : null; return proposal.indexOf(term) === 0 ? proposal : null;
})); }));
}, },
template: function (option) { template: function (value) {
return @{Html(provider.template)}; return @{Html(provider.template)};
}, },
replace: function (option) { replace: function (value) {
return '@{provider.prefix}' + @{Html(provider.replace)} + '@{provider.suffix}'; return '@{provider.prefix}' + value + '@{provider.suffix}';
}, },
index: 1 index: 1
}, },
@@ -67,6 +65,8 @@ $(function(){
url: '@context.path/upload/file/@repository.owner/@repository.name', url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: 10, maxFilesize: 10,
clickable: @clickable, clickable: @clickable,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>", previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) { success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +

View File

@@ -4,8 +4,7 @@
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.helper.html.dropdown( @gitbucket.core.helper.html.dropdown(
value = if(branch.length == 40) branch.substring(0, 10) else branch, value = if(branch.length == 40) branch.substring(0, 10) else branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree"
maxValueWidth = "200px"
) { ) {
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li> <li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li>
<li><input id="branch-control-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Find or create branch ..."/></li> <li><input id="branch-control-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Find or create branch ..."/></li>

View File

@@ -3,12 +3,12 @@
<div class="input-group" style="margin-bottom: 0px;"> <div class="input-group" style="margin-bottom: 0px;">
@html @html
<span class="input-group-btn"> <span class="input-group-btn">
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"} <span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span> data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
</span> </span>
</div> </div>
} else { } else {
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"} <span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span> data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
} }
<script> <script>
@@ -18,7 +18,7 @@
if (document.queryCommandSupported('copy')) { if (document.queryCommandSupported('copy')) {
var title = $('#@copyButtonId').attr('title'); var title = $('#@copyButtonId').attr('title');
$('#@copyButtonId').tooltip({ $('#@copyButtonId').tooltip({
@* if default container is used then 2 lines tooltip text is displayed because tooptip width is narrow. *@ @* if default container is used then 2 lines tooltip text is displayd because tooptip width is narrow. *@
container: 'body' container: 'body'
}); });
$('#@copyButtonId').on('click', function() { $('#@copyButtonId').on('click', function() {

View File

@@ -10,7 +10,7 @@
@import org.eclipse.jgit.diff.DiffEntry.ChangeType @import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){ @if(showIndex){
<div class="pull-right" style="margin-bottom: 10px;"> <div class="pull-right" style="margin-bottom: 10px;">
<div class="btn-group" data-toggle="buttons"> <div class="btn-group" data-toggle="buttons-radio">
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified"> <input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split"> <input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
</div> </div>
@@ -151,21 +151,20 @@ $(function(){
} }
window.viewType = 1; window.viewType = 1;
if(("&" + location.search.substring(1)).indexOf("&diff=split") != -1){ if(("&" + location.search.substring(1)).indexOf("&diff=split") != -1){
$('.container').removeClass('container').addClass('container-wide');
window.viewType = 0; window.viewType = 0;
} }
renderDiffs(); renderDiffs();
$('#btn-unified').click(function(){ $('#btn-unified').click(function(){
window.viewType = 1; window.viewType = 1;
$('#btn-unified').addClass('active'); $('.container-wide').removeClass('container-wide').addClass('container');
$('#btn-split').removeClass('active');
renderDiffs(); renderDiffs();
}); });
$('#btn-split').click(function(){ $('#btn-split').click(function(){
window.viewType = 0; window.viewType = 0;
$('#btn-unified').removeClass('active'); $('.container').removeClass('container').addClass('container-wide');
$('#btn-split').addClass('active');
renderDiffs(); renderDiffs();
}); });
@@ -175,7 +174,6 @@ $(function(){
} }
$(this).closest('table').find('.not-diff').toggle(); $(this).closest('table').find('.not-diff').toggle();
}); });
$('.ignore-whitespace').change(function() { $('.ignore-whitespace').change(function() {
renderOneDiff($(this).closest("table").find(".diffText"), viewType); renderOneDiff($(this).closest("table").find(".diffText"), viewType);
}); });
@@ -190,56 +188,22 @@ $(function(){
} }
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>'); return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
} }
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) { if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
$('#comment-list').children('.inline-comment').hide(); $('#comment-list').children('.inline-comment').hide();
} }
function showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr){
// assemble Ajax url
var url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (!isNaN(oldLineNumber) && oldLineNumber) {
url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
// send Ajax request
$.get(url, { dataType : 'html' }, function(responseContent) {
// create container
var tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
// add comment textarea
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
// hide reply comment field
$(tmp).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').hide();
// focus textarea
tmp.find('textarea').focus();
});
}
// Add comment button
$('.diff-outside').on('click','table.diff .add-comment',function() { $('.diff-outside').on('click','table.diff .add-comment',function() {
var $this = $(this); var $this = $(this);
var $tr = $this.closest('tr'); var $tr = $this.closest('tr');
var $check = $this.closest('table:not(.diff)').find('.toggle-notes'); var $check = $this.closest('table:not(.diff)').find('.toggle-notes');
//var url = ''; var url = '';
if (!$check.prop('checked')) { if (!$check.prop('checked')) {
$check.prop('checked', true).trigger('change'); $check.prop('checked', true).trigger('change');
} }
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) { if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
var commitId = $this.closest('.table-bordered').attr('commitId'), var commitId = $this.closest('.table-bordered').attr('commitId'),
fileName = $this.closest('.table-bordered').attr('fileName'), fileName = $this.closest('.table-bordered').attr('fileName'),
oldLineNumber, newLineNumber; oldLineNumber, newLineNumber,
url = '@helpers.url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
if (viewType == 0) { if (viewType == 0) {
oldLineNumber = $this.parent().prev('.oldline').attr('line-number'); oldLineNumber = $this.parent().prev('.oldline').attr('line-number');
newLineNumber = $this.parent().prev('.newline').attr('line-number'); newLineNumber = $this.parent().prev('.newline').attr('line-number');
@@ -247,27 +211,30 @@ $(function(){
oldLineNumber = $this.parent().prevAll('.oldline').attr('line-number'); oldLineNumber = $this.parent().prevAll('.oldline').attr('line-number');
newLineNumber = $this.parent().prevAll('.newline').attr('line-number'); newLineNumber = $this.parent().prevAll('.newline').attr('line-number');
} }
if (!isNaN(oldLineNumber) && oldLineNumber) {
showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr); url += ('&oldLineNumber=' + oldLineNumber)
}
if (!isNaN(newLineNumber) && newLineNumber) {
url += ('&newLineNumber=' + newLineNumber)
}
$.get(url, { dataType : 'html' }, function(responseContent) {
var tmp;
if (!isNaN(oldLineNumber) && oldLineNumber) {
if (!isNaN(newLineNumber) && newLineNumber) {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
} else {
tmp = getInlineContainer('new');
}
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
$tr.nextAll(':not(.not-diff):first').before(tmp);
});
} }
}).on('click', 'table.diff .btn-default', function() { }).on('click', 'table.diff .btn-default', function() {
// Cancel comment form
$(this).closest('.not-diff').prev().find('.reply-comment').closest('.not-diff').show();
$(this).closest('.inline-comment-form').remove(); $(this).closest('.inline-comment-form').remove();
}); });
// Reply comment
$('.diff-outside').on('click', '.reply-comment',function(){
var $this = $(this);
var $tr = $this.closest('tr');
var commitId = $this.closest('.table-bordered').attr('commitId');
var fileName = $this.data('filename');
var oldLineNumber = $this.data('oldline');
var newLineNumber = $this.data('newline');
showCommentForm(commitId, fileName, oldLineNumber, newLineNumber, $tr);
});
function renderOneCommitCommentIntoDiff($v, diff){ function renderOneCommitCommentIntoDiff($v, diff){
var filename = $v.attr('filename'); var filename = $v.attr('filename');
var oldline = $v.attr('oldline'); var oldline = $v.attr('oldline');
@@ -290,7 +257,6 @@ $(function(){
tmp.hide(); tmp.hide();
} }
} }
function renderStatBar(add, del){ function renderStatBar(add, del){
if(add + del > 5){ if(add + del > 5){
if(add){ if(add){
@@ -316,7 +282,6 @@ $(function(){
} }
return ret; return ret;
} }
function renderOneDiff(diffText, viewType){ function renderOneDiff(diffText, viewType){
var table = diffText.closest("table[data-diff-id]"); var table = diffText.closest("table[data-diff-id]");
var i = table.data("diff-id"); var i = table.data("diff-id");
@@ -340,59 +305,12 @@ $(function(){
} }
}); });
} }
return table;
} }
function renderReplyComment($table){
var elements = {};
var filename, newline, oldline;
$table.find('.comment-box-container .inline-comment').each(function(i, e){
filename = $(e).attr('filename');
newline = $(e).attr('newline');
oldline = $(e).attr('oldline');
var key = filename + '-' + newline + '-' + oldline;
elements[key] = {
element: $(e),
filename: filename,
newline: newline,
oldline: oldline
};
});
for(var key in elements){
filename = elements[key]['filename'];
oldline = elements[key]['oldline'];
newline = elements[key]['newline'];
var $v = $('<div class="commit-comment-box reply-comment-box">')
.append($('<input type="text" class="form-control reply-comment" placeholder="Reply...">')
.data('filename', filename)
.data('newline', newline)
.data('oldline', oldline));
var tmp;
if (typeof oldline !== 'undefined') {
if (typeof newline !== 'undefined') {
tmp = getInlineContainer();
} else {
tmp = getInlineContainer('old');
}
tmp.children('td:first').html($v);
} else {
tmp = getInlineContainer('new');
tmp.children('td:last').html($v);
}
elements[key]['element'].closest('.not-diff').after(tmp);
}
}
function renderDiffs(){ function renderDiffs(){
var i = 0, diffs = $('.diffText'); var i = 0, diffs = $('.diffText');
function render(){ function render(){
if(diffs[i]){ if(diffs[i]){
var $table = renderOneDiff($(diffs[i]), viewType); renderOneDiff($(diffs[i]), viewType);
@if(hasWritePermission) {
renderReplyComment($table);
}
i++; i++;
setTimeout(render); setTimeout(render);
} }

View File

@@ -1,12 +1,11 @@
@(value : String = "", @(value : String = "",
prefix: String = "", prefix: String = "",
style : String = "", style : String = "",
maxValueWidth : String = "",
right : Boolean = false, right : Boolean = false,
filter: (String, String) = ("",""))(body: Html) filter: (String, String) = ("",""))(body: Html)
@defining(if(filter._1.isEmpty) "" else filter._1 + "-" + scala.util.Random.alphanumeric.take(4).mkString){ filterId => @defining(if(filter._1.isEmpty) "" else filter._1 + "-" + scala.util.Random.alphanumeric.take(4).mkString){ filterId =>
<div class="btn-group" @if(style.nonEmpty){style="@style"}> <div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button id = "test" <button
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown"> class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
<i class="octicon octicon-gear"></i> <i class="octicon octicon-gear"></i>
@@ -14,10 +13,7 @@
@if(prefix.nonEmpty){ @if(prefix.nonEmpty){
<span class="muted">@prefix:</span> <span class="muted">@prefix:</span>
} }
<span class="strong" <span class="strong">@value</span>
@if(maxValueWidth.nonEmpty){style="display:inline-block; vertical-align:bottom; overflow-x:hidden; max-width:@maxValueWidth; text-overflow:ellipsis"}>
@value
</span>
} }
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@@ -30,7 +26,7 @@
</div> </div>
@if(filterId.nonEmpty) { @if(filterId.nonEmpty) {
<script> <script>
$(window).on('load', function(){ $(window).load(function(){
$('#@{filterId}-input').parent().click(function(e) { $('#@{filterId}-input').parent().click(function(e) {
e.stopPropagation(); e.stopPropagation();
}); });

View File

@@ -2,10 +2,10 @@
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<id>tag:@context.host,2013:gitbucket</id> <id>tag:@context.host,2013:gitbucket</id>
<title>GitBucket's activities</title> <title>Gitbucket's activities</title>
<link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/> <link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/>
<author> <author>
<name>GitBucket</name> <name>Gitbucket</name>
<uri>@context.baseUrl</uri> <uri>@context.baseUrl</uri>
</author> </author>
<updated>@helpers.datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max)</updated> <updated>@helpers.datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max)</updated>

View File

@@ -16,7 +16,7 @@
<div class="tabbable"> <div class="tabbable">
<ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;"> <ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;">
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li> <li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
<li><a href="#tab@(uid + 1)" data-toggle="tab" id="preview@uid">Preview</a></li> <li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" style="margin-top: 4px;" id="tab@uid"> <div class="tab-pane active" style="margin-top: 4px;" id="tab@uid">
@@ -32,7 +32,7 @@
generateScript = !enableWikiLink generateScript = !enableWikiLink
)(textarea) )(textarea)
</div> </div>
<div class="tab-pane" id="tab@(uid + 1)"> <div class="tab-pane" id="tab@(uid+1)">
<div class="markdown-body" id="preview-area@uid"> <div class="markdown-body" id="preview-area@uid">
</div> </div>
</div> </div>

View File

@@ -201,6 +201,7 @@ $(function(){
$.post('@helpers.url(repository)/issue_comments/delete/' + id, $.post('@helpers.url(repository)/issue_comments/delete/' + id,
function(data){ function(data){
if(data > 0) { if(data > 0) {
$('#comment-' + id).prev('div.issue-avatar-image').remove();
$('#comment-' + id).remove(); $('#comment-' + id).remove();
} }
}); });
@@ -229,6 +230,7 @@ $(function(){
function(data){ function(data){
if(data > 0) { if(data > 0) {
$('.commit-comment-' + id).closest('.not-diff').remove(); $('.commit-comment-' + id).closest('.not-diff').remove();
$('.commit-comment-' + id).closest('.inline-comment').remove();
} }
}); });
} }

View File

@@ -1,22 +1,12 @@
@(content: String, commentId: Int, @(content: String, commentId: Int,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
<span id="error-edit-content-@commentId" class="error"></span> <span id="error-edit-content-@commentId" class="error"></span>
@gitbucket.core.helper.html.preview( @gitbucket.core.helper.html.attached(repository, "issues"){
repository = repository, <textarea id="edit-content-@commentId" class="form-control">@content</textarea>
content = content, }
enableWikiLink = false, <div>
enableRefsLink = true, <input type="button" id="cancel-comment-@commentId" class="btn btn-danger" value="Cancel"/>
enableLineBreaks = true, <input type="button" id="update-comment-@commentId" class="btn btn-default pull-right" value="Update comment"/>
enableTaskList = true,
hasWritePermission = true,
completionContext = "issues",
style = "",
elastic = true,
tabIndex = 1
)
<div class="pull-right">
<input type="button" id="cancel-comment-@commentId" class="btn btn-default" value="Cancel"/>
<input type="button" id="update-comment-@commentId" class="btn btn-success" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
@@ -27,14 +17,13 @@ $(function(){
}; };
$('#update-comment-@commentId').click(function(){ $('#update-comment-@commentId').click(function(){
var content = $(this).parent().parent().find('textarea[name=content]').val();
$('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled'); $('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@context.path/@repository.owner/@repository.name/issue_comments/edit/@commentId', url: '@context.path/@repository.owner/@repository.name/issue_comments/edit/@commentId',
type: 'POST', type: 'POST',
data: { data: {
issueId : 0, // TODO issueId : 0, // TODO
content : content content : $('#edit-content-@commentId').val()
} }
}).done( }).done(
callback callback

View File

@@ -1,37 +1,27 @@
@(content: Option[String], issueId: Int, @(content: Option[String], issueId: Int,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.helper.html.preview( @gitbucket.core.helper.html.attached(repository, "issues"){
repository = repository, <textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
content = content.getOrElse(""), }
enableWikiLink = false, <div>
enableRefsLink = true, <input type="button" id="cancel-issue" class="btn btn-danger" value="Cancel"/>
enableLineBreaks = true, <input type="button" id="update-issue" class="btn btn-default pull-right" value="Update comment"/>
enableTaskList = true,
hasWritePermission = true,
completionContext = "issues",
style = "",
elastic = true,
tabIndex = 1
)
<div class="pull-right">
<input type="button" id="cancel-issue" class="btn btn-default" value="Cancel"/>
<input type="button" id="update-issue" class="btn btn-success" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
var callback = function(data){ var callback = function(data){
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
prettyPrint();
}; };
$('#update-issue').click(function(){ $('#update-issue').click(function(){
var content = $(this).parent().parent().find('textarea[name=content]').val();
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@context.path/@repository.owner/@repository.name/issues/edit/@issueId', url: '@context.path/@repository.owner/@repository.name/issues/edit/@issueId',
type: 'POST', type: 'POST',
data: { content : content } data: {
content : $('#edit-content').val()
}
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {

View File

@@ -148,8 +148,8 @@
<input type="hidden" name="assignedUserName" value=""/> <input type="hidden" name="assignedUserName" value=""/>
} }
@issue.map { issue => @issue.map { issue =>
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent => @gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebar =>
@sidebarComponent(issue, repository, context) @sidebar(issue, repository, context)
} }
<hr/> <hr/>
<div style="margin-bottom: 14px;"> <div style="margin-bottom: 14px;">

Some files were not shown because too many files have changed in this diff Show More