diff options
501 files changed, 8036 insertions, 4287 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000000..021acdeca39 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,49 @@ +before_script: + - export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin + - ruby -v + - which ruby + - gem install bundler + - which bundle + - echo $PATH + - cp config/database.yml.mysql config/database.yml + - cp config/gitlab.yml.example config/gitlab.yml + - ! 'sed "s/username\:.*$/username\: runner/" -i config/database.yml' + - ! 'sed "s/password\:.*$/password\: ''password''/" -i config/database.yml' + - sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml + - touch log/application.log + - touch log/test.log + - bundle install --without postgres production --jobs $(nproc) + - bundle exec rake db:create RAILS_ENV=test +jobs: +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec + name: Rspec + branches: true + tags: false + runner: ruby,mysql +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach + name: Spinach + branches: true + tags: false + runner: ruby,mysql +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake jasmine:ci + name: Jasmine + branches: true + tags: false + runner: ruby,mysql +- script: + - bundle exec rubocop + name: Rubocop + branches: true + tags: false + runner: ruby,mysql +- script: + - bundle exec rake brakeman + name: Brakeman + branches: true + tags: false + runner: ruby,mysql +deploy_jobs: [] +skip_refs: '' diff --git a/CHANGELOG b/CHANGELOG index ef0f164264e..3df8e7ea516 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,14 +1,83 @@ Please view this file on the master branch, on stable branches it's out of date. -v 7.11.0 (unreleased) +v 7.12.0 (unreleased) + - Don't notify users mentioned in code blocks or blockquotes. + - Omit link to generate labels if user does not have access to create them (Stan Hu) + - Disable changing of the source branch in merge request update API (Stan Hu) + - Shorten merge request WIP text. + - Add option to disallow users from registering any application to use GitLab as an OAuth provider + - Support editing target branch of merge request (Stan Hu) + - Refactor permission checks with issues and merge requests project settings (Stan Hu) + - Fix Markdown preview not working in Edit Milestone page (Stan Hu) + - Fix Zen Mode not closing with ESC key (Stan Hu) + - Allow HipChat API version to be blank and default to v2 (Stan Hu) + - Add file attachment support in Milestone description (Stan Hu) + - Fix milestone "Browse Issues" button. + - Set milestone on new issue when creating issue from index with milestone filter active. + - Make namespace API available to all users (Stan Hu) + - Add web hook support for note events (Stan Hu) + - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) + - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) + - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) + - Fix git blame syntax highlighting when different commits break up lines (Stan Hu) + - Add "Resend confirmation e-mail" link in profile settings (Stan Hu) + - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) + - Disabled expansion of top/bottom blobs for new file diffs + - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) + - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka) + - Use the user list from the target project in a merge request (Stan Hu) + - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen) + - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen) + - Fix new/empty milestones showing 100% completion value (Jonah Bishop) + - Add a note when an Issue or Merge Request's title changes + - Consistently refer to MRs as either Accepted or Rejected. + - Add Accepted and Rejected tabs to MR lists. + - Prefix EmailsOnPush email subject with `[Git]`. + - Group project contributions by both name and email. + - Clarify navigation labels for Project Settings and Group Settings. + - Move user avatar and logout button to sidebar + - You can not remove user if he/she is an only owner of group + - User should be able to leave group. If not - show him proper message + - User has ability to leave project + - Add SAML support as an omniauth provider + - Allow to configure a URL to show after sign out + - Add an option to automatically sign-in with an Omniauth provider + - Better performance for web editor (switched from satellites to rugged) + - GitLab CI service sends .gitlab-ci.yaml in each push call + - When remove project - move repository and schedule it removal + - Improve group removing logic + - Trigger create-hooks on backup restore task + +v 7.11.4 + - Fix missing bullets when creating lists + - Set rel="nofollow" on external links + +v 7.11.3 + - no changes + - Fix upgrader script (Martins Polakovs) + +v 7.11.2 + - no changes + +v 7.11.1 + - no changes + +v 7.11.0 + - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) + - Get editing comments to work in Chrome 43 again. + - Allow special character in users bio. I.e.: I <3 GitLab + +v 7.11.0 + - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) + - Don't show duplicate deploy keys + - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) - Make the first branch pushed to an empty repository the default HEAD (Stan Hu) + - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu) - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) - Add application setting to restrict user signups to e-mail domains (Stan Hu) - Don't allow a merge request to be merged when its title starts with "WIP". - Add a page title to every page. - Allow primary email to be set to an email that you've already added. - - Fix Error 500 when searching Wiki pages (Stan Hu) - - Get Gitorious importer to work again. - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - Ignore invalid lines in .gitmodules - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) @@ -17,26 +86,29 @@ v 7.11.0 (unreleased) - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu) - Improve project page UI - Fix broken file browsing with relative submodule in personal projects (Stan Hu) - - Fix DB error when trying to tag a repository (Stan Hu) - Add "Reply quoting selected text" shortcut key (`r`) - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention. - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention. - When use change branches link at MR form - save source branch selection instead of target one - Improve handling of large diffs - - + - Added GitLab Event header for project hooks + - Add Two-factor authentication (2FA) for GitLab logins - Show Atom feed buttons everywhere where applicable. - Add project activity atom feed. - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits. + - Explain how to get a new password reset token in welcome emails - Include commit comments in MR from a forked project. - - Fix adding new group members from admin area - Group milestones by title in the dashboard and all other issue views. - Query issues, merge requests and milestones with their IID through API (Julien Bianchi) - Add default project and snippet visibility settings to the admin web UI. - Show incompatible projects in Google Code import status (Stan Hu) - Fix bug where commit data would not appear in some subdirectories (Stan Hu) - - Unescape branch names in compare commit (Stan Hu) - Task lists are now usable in comments, and will show up in Markdown previews. + - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu) - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) + - Protect OmniAuth request phase against CSRF. + - Don't send notifications to mentioned users that don't have access to the project in question. + - Add search issues/MR by number - Move snippets UI to fluid layout - Improve UI for sidebar. Increase separation between navigation and content - Improve new project command options (Ben Bodenmiller) @@ -48,6 +120,25 @@ v 7.11.0 (unreleased) - Add footnotes support to Markdown (Guillaume Delbergue) - Add current_sign_in_at to UserFull REST api. - Make Sidekiq MemoryKiller shutdown signal configurable + - Add "Create Merge Request" buttons to commits and branches pages and push event. + - Show user roles by comments. + - Fix automatic blocking of auto-created users from Active Directory. + - Call merge request web hook for each new commits (Arthur Gautier) + - Use SIGKILL by default in Sidekiq::MemoryKiller + - Fix mentioning of private groups. + - Add style for <kbd> element in markdown + - Spin spinner icon next to "Checking for CI status..." on MR page. + - Fix reference links in dashboard activity and ATOM feeds. + - Ensure that the first added admin performs repository imports + +v 7.10.4 + - Fix migrations broken in 7.10.2 + - Make tags for GitLab installations running on MySQL case sensitive + - Get Gitorious importer to work again. + - Fix adding new group members from admin area + - Fix DB error when trying to tag a repository (Stan Hu) + - Fix Error 500 when searching Wiki pages (Stan Hu) + - Unescape branch names in compare commit (Stan Hu) - Order commit comments chronologically in API. v 7.10.2 @@ -130,12 +221,12 @@ v 7.10.0 - Ability to skip some items from backup (database, respositories or uploads) - Archive repositories in background worker. - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. - - Project labels are now available over the API under the "tag_list" field (Cristian Medina) + - Project labels are now available over the API under the "tag_list" field (Cristian Medina) - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) - Fix and improve help rendering (Sullivan Sénéchal) - Fix final line in EmailsOnPush email diff being rendered as error. - Prevent duplicate Buildkite service creation. - - Fix git over ssh errors 'fatal: protocol error: bad line length character' + - Fix git over ssh errors 'fatal: protocol error: bad line length character' - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled - Bust group page project list cache when namespace name or path changes. - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded @@ -144,7 +235,7 @@ v 7.10.0 - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix merge request comments on files with multiple commits - Fix Resource Owner Password Authentication Flow - + v 7.9.4 - Security: Fix project import URL regex to prevent arbitary local repos from being imported - Fixed issue where only 25 commits would load in file listings @@ -449,6 +540,12 @@ v 7.5.0 - Use secret token with GitLab internal API. - Add missing timestamps to 'members' table +v 7.4.5 + - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) + +v 7.4.4 + - No changes + v 7.4.3 - Fix raw snippets view - Fix security issue for member api diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3165b7379d3..38fa66816a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,11 +29,9 @@ You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq ## Issue tracker -To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). +To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/). -The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. - -Issues can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) or [github.com](https://github.com/gitlabhq/gitlabhq/issues). +The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple. @@ -63,7 +61,7 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. -To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. +To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. ### Merge request guidelines @@ -86,7 +84,9 @@ If you can, please submit a merge request with the fix or improvements including 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). 1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk. -The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. +The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. +Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases. +After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it. @@ -160,6 +160,8 @@ If you add a dependency in GitLab (such as an operating system package) please c 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript) 1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security 1. [Markdown](http://www.cirosantilli.com/markdown-styleguide) +1. [Database Migrations](doc/development/migration_style_guide.md) +1. [Documentation styleguide](doc_styleguide.md) 1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing). This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com). @@ -176,4 +178,4 @@ Project maintainers have the right and responsibility to remove, edit, or reject Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com -This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) +This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
\ No newline at end of file diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 097a15a2af3..ec1cf33c3f6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.2 +2.6.3 @@ -1,18 +1,7 @@ source "https://rubygems.org" -def darwin_only(require_as) - RUBY_PLATFORM.include?('darwin') && require_as -end - -def linux_only(require_as) - RUBY_PLATFORM.include?('linux') && require_as -end - gem "rails", "~> 4.1.0" -# Make links from text -gem 'rails_autolink', '~> 1.1' - # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -23,34 +12,48 @@ gem "pg", group: :postgres # Auth gem "devise", '3.2.4' gem "devise-async", '0.9.0' -gem 'omniauth', "~> 1.1.3" +gem 'omniauth', "~> 1.2.2" gem 'omniauth-google-oauth2' gem 'omniauth-twitter' gem 'omniauth-github' gem 'omniauth-shibboleth' -gem 'omniauth-kerberos' +gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-gitlab' gem 'omniauth-bitbucket' +gem 'omniauth-saml' gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" +# Two-factor authentication +gem 'devise-two-factor' +gem 'rqrcode-rails3' +gem 'attr_encrypted', '1.3.4' + # Browser detection gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.10' +gem "gitlab_git", '~> 7.2.2' # Ruby/Rack Git Smart-HTTP Server Handler +# GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) +# For full list of changes see https://github.com/SaitoWu/grack/compare/master...gitlabhq:master gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # LDAP Auth +# GitLab fork with several improvements to original library. For full list of changes +# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 4.0.2' # Language detection +# GitLab fork of linguist does not require pygments/python dependency. +# New version of original gem also dropped pygments support but it has strict +# dependency to unstable rugged version. We have internal issue for replacing +# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052. gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API @@ -89,7 +92,7 @@ gem "seed-fu" # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' -gem 'task_list', '~> 1.0.0', require: 'task_list/railtie' +gem 'task_list', '1.0.2', require: 'task_list/railtie' gem 'github-markup' gem 'redcarpet', '~> 3.2.3' gem 'RedCloth' @@ -97,7 +100,7 @@ gem 'rdoc', '~>3.6' gem 'org-ruby', '= 0.9.12' gem 'creole', '~>0.3.6' gem 'wikicloth', '=0.8.1' -gem 'asciidoctor', '= 0.1.4' +gem 'asciidoctor', '~> 1.5.2' # Diffs gem 'diffy', '~> 3.0.3' @@ -167,7 +170,7 @@ gem "underscore-rails", "~> 1.4.4" gem "sanitize", '~> 2.0' # Protect against bruteforcing -gem "rack-attack" +gem "rack-attack", '~> 4.3.0' # Ace editor gem 'ace-rails-ap' @@ -181,23 +184,23 @@ gem 'charlock_holmes' gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" -gem 'turbolinks' +gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks' -gem 'select2-rails' +gem 'addressable' +gem 'bootstrap-sass', '~> 3.0' +gem 'font-awesome-rails', '~> 4.2' +gem 'gitlab_emoji', '~> 0.1' +gem 'gon', '~> 5.0.0' gem 'jquery-atwho-rails', '~> 1.0.0' -gem "jquery-rails" -gem "jquery-ui-rails" -gem "jquery-scrollto-rails" -gem "raphael-rails", "~> 2.1.2" -gem 'bootstrap-sass', '~> 3.0' -gem "font-awesome-rails", '~> 4.2' -gem "gitlab_emoji", "~> 0.1" -gem "gon", '~> 5.0.0' +gem 'jquery-rails', '3.1.2' +gem 'jquery-scrollto-rails' +gem 'jquery-ui-rails' gem 'nprogress-rails' +gem 'raphael-rails', '~> 2.1.2' gem 'request_store' -gem "virtus" -gem 'addressable' +gem 'select2-rails' +gem 'virtus' group :development do gem 'brakeman', require: false @@ -234,25 +237,18 @@ group :development, :test do gem 'minitest', '~> 5.3.0' # Generate Fake data - gem "ffaker" - - # Guard - gem 'guard-rspec' - gem 'guard-spinach' - - # Notification - gem 'rb-fsevent', require: darwin_only('rb-fsevent') - gem 'growl', require: darwin_only('growl') - gem 'rb-inotify', require: linux_only('rb-inotify') + gem 'ffaker', '~> 2.0.0' # PhantomJS driver for Capybara gem 'poltergeist', '~> 1.5.1' - gem 'jasmine-rails' + gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon-jasmine' - gem "spring", '~> 1.3.1' - gem "spring-commands-rspec", '1.0.4' - gem "spring-commands-spinach", '1.0.0' + gem 'spring', '~> 1.3.1' + gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-teaspoon', '~> 0.0.2' gem "byebug" end @@ -272,4 +268,4 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' -gem "rugments" +gem "rugments", "~> 1.0.0.beta7" diff --git a/Gemfile.lock b/Gemfile.lock index 6f58c4f4fda..a341a5df409 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,10 +42,12 @@ GEM arel (5.0.1.20140414130214) asana (0.0.6) activeresource (>= 3.2.3) - asciidoctor (0.1.4) + asciidoctor (1.5.2) ast (2.0.0) astrolabe (1.3.0) parser (>= 2.2.0.pre.3, < 3.0) + attr_encrypted (1.3.4) + encryptor (>= 1.3.0) attr_required (1.0.0) autoprefixer-rails (5.1.11) execjs @@ -99,13 +101,13 @@ GEM coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.0.1) + coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.9.1.1) colored (1.2) colorize (0.5.8) columnize (0.9.0) @@ -136,6 +138,13 @@ GEM warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) + devise-two-factor (1.0.1) + activemodel + activesupport + attr_encrypted (~> 1.3.2) + devise (~> 3.2.4) + rails + rotp (~> 1.6.1) diff-lcs (1.2.5) diffy (3.0.3) docile (1.1.5) @@ -147,6 +156,7 @@ GEM email_spec (1.5.0) launchy (~> 2.1) mail (~> 2.2) + encryptor (1.3.0) enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) @@ -166,7 +176,7 @@ GEM faraday_middleware (0.9.0) faraday (>= 0.7.4, < 0.9) fastercsv (1.5.5) - ffaker (1.22.1) + ffaker (2.0.0) ffi (1.9.8) fog (1.21.0) fog-brightbox @@ -215,11 +225,11 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.10) + gitlab_git (7.2.2) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) - rugged (~> 0.21.2) + rugged (~> 0.22.2) gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) @@ -251,19 +261,6 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - growl (1.0.3) - guard (2.2.4) - formatador (>= 0.2.4) - listen (~> 2.1) - lumberjack (~> 1.0) - pry (>= 0.9.12) - thor (>= 0.18.1) - guard-rspec (4.2.0) - guard (>= 2.1.1) - rspec (>= 2.14, < 4.0) - guard-spinach (0.0.2) - guard (>= 1.1) - spinach haml (4.0.5) tilt haml-rails (0.5.3) @@ -290,14 +287,8 @@ GEM i18n (0.7.0) ice_cube (0.11.1) ice_nine (0.10.0) - jasmine-core (2.2.0) - jasmine-rails (0.10.8) - jasmine-core (>= 1.3, < 3.0) - phantomjs (>= 1.9) - railties (>= 3.2.0) - sprockets-rails jquery-atwho-rails (1.0.1) - jquery-rails (3.1.0) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -322,13 +313,14 @@ GEM celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - lumberjack (1.0.4) + macaddr (1.7.1) + systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) - mini_portile (0.6.1) + mini_portile (0.6.2) minitest (5.3.5) mousetrap-rails (1.4.6) multi_json (1.10.1) @@ -340,7 +332,7 @@ GEM net-ssh (>= 2.6.5) net-ssh (2.8.0) newrelic_rpm (3.9.4.245) - nokogiri (1.6.5) + nokogiri (1.6.6.2) mini_portile (~> 0.6.0) nprogress-rails (0.1.2.3) oauth (0.4.7) @@ -352,9 +344,9 @@ GEM rack (~> 1.2) octokit (3.7.0) sawyer (~> 0.6.0, >= 0.5.3) - omniauth (1.1.4) - hashie (>= 1.2, < 3) - rack + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) omniauth-bitbucket (0.0.2) multi_json (~> 1.7) omniauth (~> 1.1) @@ -379,6 +371,9 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) + omniauth-saml (1.3.1) + omniauth (~> 1.1) + ruby-saml (~> 0.8.1) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) @@ -390,7 +385,6 @@ GEM parser (2.2.0.2) ast (>= 1.1, < 3.0) pg (0.15.1) - phantomjs (1.9.8.0) poltergeist (1.5.1) capybara (~> 2.1) cliver (~> 0.3.1) @@ -408,10 +402,10 @@ GEM quiet_assets (1.0.2) railties (>= 3.1, < 5.0) racc (1.4.10) - rack (1.5.2) + rack (1.5.3) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.2.0) + rack-attack (4.3.0) rack rack-cors (0.2.9) rack-mini-profiler (0.9.0) @@ -440,8 +434,6 @@ GEM sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - rails_autolink (1.1.6) - rails (> 3.1) railties (4.1.9) actionpack (= 4.1.9) activesupport (= 4.1.9) @@ -482,11 +474,11 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) + rotp (1.6.1) rouge (1.7.7) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) + rqrcode (0.4.2) + rqrcode-rails3 (0.1.7) + rqrcode (>= 0.4.2) rspec-collection_matchers (1.1.2) rspec-expectations (>= 2.99.0.beta1) rspec-core (2.99.2) @@ -509,6 +501,9 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.1) + ruby-saml (0.8.2) + nokogiri (>= 1.5.0) + uuid (~> 2.3) ruby2ruby (2.1.3) ruby_parser (~> 3.1) sexp_processor (~> 4.0) @@ -516,8 +511,8 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.0) rubypants (0.2.0) - rugged (0.21.4) - rugments (1.0.0.beta6) + rugged (0.22.2) + rugments (1.0.0.beta7) safe_yaml (0.9.7) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -533,9 +528,9 @@ GEM sdoc (0.3.20) json (>= 1.1.3) rdoc (~> 3.10) - seed-fu (2.3.1) - activerecord (>= 3.1, < 4.2) - activesupport (>= 3.1, < 4.2) + seed-fu (2.3.5) + activerecord (>= 3.1, < 4.3) + activesupport (>= 3.1, < 4.3) select2-rails (3.5.2) thor (~> 0.14) settingslogic (2.0.9) @@ -575,11 +570,13 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) - spring (1.3.3) + spring (1.3.6) spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-spinach (1.0.0) spring (>= 0.9.1) + spring-commands-teaspoon (0.0.2) + spring (>= 0.9.1) sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) @@ -592,8 +589,13 @@ GEM stamp (0.5.0) state_machine (1.2.0) stringex (2.5.2) + systemu (2.6.5) task_list (1.0.2) html-pipeline + teaspoon (1.0.2) + railties (>= 3.2.5, < 5) + teaspoon-jasmine (2.2.0) + teaspoon (>= 1.0.0) temple (0.6.7) term-ansicolor (1.2.2) tins (~> 0.8) @@ -619,7 +621,7 @@ GEM multi_json (~> 1.7) twitter-stream (~> 0.1) tins (0.13.1) - turbolinks (2.0.0) + turbolinks (2.5.3) coffee-rails twitter-stream (0.1.16) eventmachine (>= 0.12.8) @@ -640,6 +642,8 @@ GEM raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) + uuid (2.3.7) + macaddr (~> 1.0) version_sorter (2.0.0) virtus (1.0.1) axiom-types (~> 0.0.5) @@ -669,7 +673,8 @@ DEPENDENCIES addressable annotate (~> 2.6.0.beta2) asana (~> 0.0.6) - asciidoctor (= 0.1.4) + asciidoctor (~> 1.5.2) + attr_encrypted (= 1.3.4) awesome_print better_errors binding_of_caller @@ -691,13 +696,14 @@ DEPENDENCIES default_value_for (~> 3.0.0) devise (= 3.2.4) devise-async (= 0.9.0) + devise-two-factor diffy (~> 3.0.3) doorkeeper (= 2.1.3) dropzonejs-rails email_spec enumerize factory_girl_rails - ffaker + ffaker (~> 2.0.0) fog (~> 1.14) font-awesome-rails (~> 4.2) foreman @@ -707,23 +713,19 @@ DEPENDENCIES gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.10) + gitlab_git (~> 7.2.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - growl - guard-rspec - guard-spinach haml-rails hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty - jasmine-rails jquery-atwho-rails (~> 1.0.0) - jquery-rails + jquery-rails (= 3.1.2) jquery-scrollto-rails jquery-turbolinks jquery-ui-rails @@ -735,12 +737,13 @@ DEPENDENCIES newrelic_rpm nprogress-rails octokit (= 3.7.0) - omniauth (~> 1.1.3) + omniauth (~> 1.2.2) omniauth-bitbucket omniauth-github omniauth-gitlab omniauth-google-oauth2 omniauth-kerberos + omniauth-saml omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.12) @@ -748,23 +751,21 @@ DEPENDENCIES poltergeist (~> 1.5.1) pry-rails quiet_assets (~> 1.0.1) - rack-attack + rack-attack (~> 4.3.0) rack-cors rack-mini-profiler rack-oauth2 (~> 1.0.5) rails (~> 4.1.0) - rails_autolink (~> 1.1) raphael-rails (~> 2.1.2) - rb-fsevent - rb-inotify rdoc (~> 3.6) redcarpet (~> 3.2.3) redis-rails request_store rerun (~> 0.10.0) + rqrcode-rails3 rspec-rails (= 2.99) rubocop (= 0.28.0) - rugments + rugments (~> 1.0.0.beta7) sanitize (~> 2.0) sass-rails (~> 4.0.2) sdoc @@ -781,15 +782,18 @@ DEPENDENCIES slim spinach-rails spring (~> 1.3.1) - spring-commands-rspec (= 1.0.4) - spring-commands-spinach (= 1.0.0) + spring-commands-rspec (~> 1.0.0) + spring-commands-spinach (~> 1.0.0) + spring-commands-teaspoon (~> 0.0.2) stamp state_machine - task_list (~> 1.0.0) + task_list (= 1.0.2) + teaspoon (~> 1.0.0) + teaspoon-jasmine test_after_commit thin tinder (~> 1.9.2) - turbolinks + turbolinks (~> 2.5.0) uglifier underscore-rails (~> 1.4.4) unf diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 68ac3232b09..00000000000 --- a/Guardfile +++ /dev/null @@ -1,27 +0,0 @@ -# A sample Guardfile -# More info at https://github.com/guard/guard#readme - -guard 'rspec', cmd: "spring rspec", all_on_start: false, all_after_pass: false do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^lib/api/(.+)\.rb$}) { |m| "spec/requests/api/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch('config/routes.rb') { "spec/routing" } - watch('app/controllers/application_controller.rb') { "spec/controllers" } - - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end - -guard 'spinach', command_prefix: 'spring' do - watch(%r|^features/(.*)\.feature|) - watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m| - "features/#{m[1]}#{m[2]}.feature" - end -end diff --git a/README.md b/README.md index 130351b15b8..85ea5c876af 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab. ## Open source software to collaborate on code -![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif) +To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - Manage Git repositories with fine grained access controls that keep your code secure - Perform code reviews and enhance collaboration with merge requests @@ -21,7 +21,7 @@ There are two editions of GitLab. *GitLab [Community Edition](https://about.gitlab.com/features/) (CE)* is available without any costs under an MIT license. *GitLab Enterprise Edition (EE)* includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users. -To get access to the EE and support please [become a subscriber](https://about.gitlab.com/pricing/). +To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). ## Code status @@ -101,4 +101,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ## Is it awesome? Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. -[These people](https://twitter.com/gitlab/favorites) seem to like it. +[These people](https://twitter.com/gitlab/favorites) seem to like it.
\ No newline at end of file @@ -1 +1 @@ -7.11.0.pre +7.12.0.pre
\ No newline at end of file diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bb9da147018..6a3f7386d5b 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -49,8 +49,6 @@ window.slugify = (text) -> window.ajaxGet = (url) -> $.ajax({type: "GET", url: url, dataType: "script"}) -window.showAndHide = (selector) -> - window.split = (val) -> return val.split( /,\s*/ ) @@ -92,15 +90,7 @@ window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> window.sanitize = (str) -> return str.replace(/<(?:.|\n)*?>/gm, '') -window.linkify = (str) -> - exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig - return str.replace(exp,"<a href='$1'>$1</a>") - -window.simpleFormat = (str) -> - linkify(sanitize(str).replace(/\n/g, '<br />')) - window.unbindEvents = -> - $(document).unbind('scroll') $(document).off('scroll') window.shiftWindow = -> @@ -116,7 +106,10 @@ window.addEventListener "hashchange", shiftWindow $ -> # Click a .js-select-on-focus field, select the contents - $(".js-select-on-focus").on "focusin", -> $(this).select() + $(".js-select-on-focus").on "focusin", -> + # Prevent a mouseup event from deselecting the input + $(this).select().one 'mouseup', (e) -> + e.preventDefault() $('.remove-row').bind 'ajax:success', -> $(this).closest('li').fadeOut() @@ -140,8 +133,8 @@ $ -> # Place the logo tooltip on the right when collapsed, bottom when expanded $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' else - # Otherwise use the data-placement attribute like normal - $el.data('placement') + # Otherwise use the data-placement attribute, or 'bottom' if undefined + $el.data('placement') or 'bottom' }) # Form submitter @@ -174,6 +167,10 @@ $ -> $(@).next('table').show() $(@).remove() + $('.navbar-toggle').on 'click', -> + $('.header-content .title').toggle() + $('.header-content .navbar-collapse').toggle() + # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> $(@).toggleClass('active') @@ -189,14 +186,3 @@ $ -> new ConfirmDangerModal(form, text) new Aside() - -(($) -> - # Disable an element and add the 'disabled' Bootstrap class - $.fn.extend disable: -> - $(@).attr('disabled', 'disabled').addClass('disabled') - - # Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend enable: -> - $(@).removeAttr('disabled').removeClass('disabled') - -)(jQuery) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 06787ddf874..da56e3cdbc8 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -27,6 +27,7 @@ class Dispatcher new Milestone() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + new DropzoneInput($('.milestone-form')) when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' @@ -112,6 +113,13 @@ class Dispatcher new NamespaceSelect() when 'dashboard' shortcut_handler = new ShortcutsDashboardNavigation() + switch path[1] + when 'issues', 'merge_requests' + new UsersSelect() + when 'groups' + switch path[1] + when 'issues', 'merge_requests' + new UsersSelect() when 'profiles' new Profile() when 'projects' diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee index 40fb6cb9fc3..0a9db8eb5ef 100644 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ b/app/assets/javascripts/extensions/jquery.js.coffee @@ -1,13 +1,11 @@ -$.fn.showAndHide = -> - $(@).show(). - delay(3000). - fadeOut() - -$.fn.enableButton = -> - $(@).removeAttr('disabled'). - removeClass('disabled') - -$.fn.disableButton = -> - $(@).attr('disabled', 'disabled'). - addClass('disabled') +# Disable an element and add the 'disabled' Bootstrap class +$.fn.extend disable: -> + $(@) + .attr('disabled', 'disabled') + .addClass('disabled') +# Enable an element and remove the 'disabled' Bootstrap class +$.fn.extend enable: -> + $(@) + .removeAttr('disabled') + .removeClass('disabled') diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 86ad3d03bac..74d6b80be5e 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,4 +1,3 @@ -#= require jquery #= require jquery.waitforimages #= require task_list diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 7c1e2b822d7..b8f916b5223 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,17 +1,31 @@ -#= require jquery -#= require bootstrap +#= require jquery.waitforimages #= require task_list class @MergeRequest + # Initialize MergeRequest behavior + # + # Options: + # action - String, current controller action + # diffs_loaded - Boolean, have diffs been pre-rendered server-side? + # (default: true if `action` is 'diffs', otherwise false) + # commits_loaded - Boolean, have commits been pre-rendered server-side? + # (default: false) + # + # check_enable - Boolean, whether to check automerge status + # url_to_automerge_check - String, URL to use to check automerge status + # current_status - String, current automerge status + # ci_enable - Boolean, whether a CI service is enabled + # url_to_ci_check - String, URL to use to check CI status + # constructor: (@opts) -> @initContextWidget() this.$el = $('.merge-request') - @diffs_loaded = if @opts.action == 'diffs' then true else false - @commits_loaded = false - this.activateTab(@opts.action) + @diffs_loaded = @opts.diffs_loaded or @opts.action == 'diffs' + @commits_loaded = @opts.commits_loaded or false this.bindEvents() + this.activateTabFromPath() this.initMergeWidget() this.$('.show-all-commits').on 'click', => @@ -65,8 +79,18 @@ class @MergeRequest , 'json' bindEvents: -> - this.$('.merge-request-tabs').on 'click', 'li', (event) => - this.activateTab($(event.currentTarget).data('action')) + this.$('.merge-request-tabs a[data-toggle="tab"]').on 'shown.bs.tab', (e) => + $target = $(e.target) + tab_action = $target.data('action') + + # Lazy-load diffs + if tab_action == 'diffs' + this.loadDiff() unless @diffs_loaded + $('.diff-header').trigger('sticky_kit:recalc') + + # Skip tab-persisting behavior on MergeRequests#new + unless @opts.action == 'new' + @setCurrentAction(tab_action) this.$('.accept_merge_request').on 'click', -> $('.automerge_widget.can_be_merged').hide() @@ -84,21 +108,54 @@ class @MergeRequest this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_widget.failed').show() - activateTab: (action) -> - this.$('.merge-request-tabs li').removeClass 'active' - this.$('.tab-content').hide() - switch action - when 'diffs' - this.$('.merge-request-tabs .diffs-tab').addClass 'active' - this.loadDiff() unless @diffs_loaded - this.$('.diffs').show() - $(".diff-header").trigger("sticky_kit:recalc") - when 'commits' - this.$('.merge-request-tabs .commits-tab').addClass 'active' - this.$('.commits').show() - else - this.$('.merge-request-tabs .notes-tab').addClass 'active' - this.$('.notes').show() + # Activate a tab based on the current URL path + # + # If the current action is 'show' or 'new' (i.e., initial page load), + # activates the first tab, otherwise activates the tab corresponding to the + # current action (diffs, commits). + activateTabFromPath: -> + if @opts.action == 'show' || @opts.action == 'new' + this.$('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') + else + this.$(".merge-request-tabs a[data-action='#{@opts.action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one + # + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. + # + # Examples: + # + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + setCurrentAction: (action) -> + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = location.pathname.replace(/\/(commits|diffs)\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += location.search + location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, '', new_state showState: (state) -> $('.automerge_widget').hide() diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index c25b1ddb066..b9bd5c730bf 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,6 +1,4 @@ -#= require jquery #= require autosave -#= require bootstrap #= require dropzone #= require dropzone_input #= require gfm_auto_complete @@ -312,6 +310,14 @@ class @Notes form.show() textarea = form.find("textarea") textarea.focus() + + # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). + # The textarea has the correct value, Chrome just won't show it unless we + # modify it, so let's clear it and re-set it! + value = textarea.val() + textarea.val "" + textarea.val value + disableButtonIfEmptyField textarea, form.find(".js-comment-button") ### diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index de356fbec77..40459a9a155 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -12,11 +12,11 @@ class @Profile $(this).find('.update-failed').hide() $('.update-username form').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $('.js-choose-user-avatar-button').bind "click", -> diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index 6b534f29218..bb532194682 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -1,6 +1,4 @@ -#= require jquery #= require mousetrap - #= require shortcuts_navigation class @ShortcutsIssuable extends ShortcutsNavigation diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index ed12bdcef22..3be14cb43dd 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,5 +1,4 @@ #= require d3 -#= require jquery #= require stat_graph_contributors_util class @ContributorsStatGraph diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index 1670f5c7bc1..cfe5508290f 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -2,11 +2,15 @@ window.ContributorsStatGraphUtil = parse_log: (log) -> total = {} by_author = {} + by_email = {} for entry in log @add_date(entry.date, total) unless total[entry.date]? - @add_author(entry, by_author) unless by_author[entry.author_name]? - @add_date(entry.date, by_author[entry.author_name]) unless by_author[entry.author_name][entry.date] - @store_data(entry, total[entry.date], by_author[entry.author_name][entry.date]) + + data = by_author[entry.author_name] #|| by_email[entry.author_email] + data ?= @add_author(entry, by_author, by_email) + + @add_date(entry.date, data) unless data[entry.date] + @store_data(entry, total[entry.date], data[entry.date]) total = _.toArray(total) by_author = _.toArray(by_author) total: total, by_author: by_author @@ -15,10 +19,12 @@ window.ContributorsStatGraphUtil = collection[date] = {} collection[date].date = date - add_author: (author, by_author) -> - by_author[author.author_name] = {} - by_author[author.author_name].author_name = author.author_name - by_author[author.author_name].author_email = author.author_email + add_author: (author, by_author, by_email) -> + data = {} + data.author_name = author.author_name + data.author_email = author.author_email + by_author[author.author_name] = data + by_email[author.author_email] = data store_data: (entry, total, by_author) -> @store_commits(total, by_author) diff --git a/app/assets/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee index 66757565d3a..81cfc37b956 100644 --- a/app/assets/javascripts/wikis.js.coffee +++ b/app/assets/javascripts/wikis.js.coffee @@ -1,9 +1,17 @@ class @Wikis constructor: -> - $('.build-new-wiki').bind "click", -> + $('.build-new-wiki').bind "click", (e) -> + $('[data-error~=slug]').addClass("hidden") + $('p.hint').show() field = $('#new_wiki_path') - slug = field.val() - path = field.attr('data-wikis-path') + valid_slug_pattern = /^[\w\/-]+$/ - if(slug.length > 0) - location.href = path + "/" + slug + slug = field.val() + if slug.match valid_slug_pattern + path = field.attr('data-wikis-path') + if(slug.length > 0) + location.href = path + "/" + slug + else + e.preventDefault() + $('p.hint').hide() + $('[data-error~=slug]').removeClass("hidden") diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index 0fb8f7ed75f..8a0564a9098 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -1,6 +1,8 @@ -class @ZenMode - @fullscreen_prefix = 'fullscreen_' +#= require dropzone +#= require mousetrap +#= require mousetrap/pause +class @ZenMode constructor: -> @active_zen_area = null @active_checkbox = null @@ -12,34 +14,31 @@ class @ZenMode $('body').on 'click', '.zen-enter-link', (e) => e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true) + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change() $('body').on 'click', '.zen-leave-link', (e) => e.preventDefault() - $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false) + $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change() $('body').on 'change', '.zen-toggle-comment', (e) => checkbox = e.currentTarget if checkbox.checked # Disable other keyboard shortcuts in ZEN mode Mousetrap.pause() - @udpateActiveZenArea(checkbox) + @updateActiveZenArea(checkbox) else @exitZenMode() $(document).on 'keydown', (e) => - if e.keyCode is $.ui.keyCode.ESCAPE + if e.keyCode is 27 # Esc @exitZenMode() e.preventDefault() - $(window).on 'hashchange', @updateZenModeFromLocationHash - - udpateActiveZenArea: (checkbox) => + updateActiveZenArea: (checkbox) => @active_checkbox = $(checkbox) @active_checkbox.prop('checked', true) @active_zen_area = @active_checkbox.parent().find('textarea') @active_zen_area.focus() - window.location.hash = ZenMode.fullscreen_prefix + @active_checkbox.prop('id') exitZenMode: => if @active_zen_area isnt null @@ -47,21 +46,9 @@ class @ZenMode @active_checkbox.prop('checked', false) @active_zen_area = null @active_checkbox = null - window.location.hash = '' - window.scrollTo(window.pageXOffset, @scroll_position) + @restoreScroll(@scroll_position) # Enable dropzone when leaving ZEN mode Dropzone.forElement('.div-dropzone').enable() - checkboxFromLocationHash: (e) -> - id = $.trim(window.location.hash.replace('#' + ZenMode.fullscreen_prefix, '')) - if id - return $('.zennable input[type=checkbox]#' + id)[0] - else - return null - - updateZenModeFromLocationHash: (e) => - checkbox = @checkboxFromLocationHash() - if checkbox - @udpateActiveZenArea(checkbox) - else - @exitZenMode() + restoreScroll: (y) -> + window.scrollTo(window.pageXOffset, y) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index a0794e7825a..08cbe911672 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -73,6 +73,22 @@ padding: 0; } + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #FCFCFC; + border-width: 1px; + border-style: solid; + border-color: #CCC #CCC #BBB; + border-image: none; + border-radius: 3px; + box-shadow: 0px -1px 0px #BBB inset; + } + h1 { margin-top: 45px; font-size: 2.5em; diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 596376c3970..3d7868fb7d2 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,10 +1,11 @@ $style_color: #474D57; -$hover: #FFF3EB; +$hover: #FFFAF1; $gl-text-color: #222222; $gl-link-color: #446e9b; $nprogress-color: #c0392b; $gl-font-size: 14px; $list-font-size: 15px; +$sidebar_collapsed_width: 52px; $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 1e569978cc8..1419a9cded9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -307,7 +307,7 @@ table { } .btn-sign-in { - margin-top: 5px; + margin-top: 7px; text-shadow: none; } @@ -342,9 +342,8 @@ table { } #nprogress .spinner { - top: auto !important; - bottom: 20px !important; - left: 20px !important; + top: 15px !important; + right: 10px !important; } .header-with-avatar { diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 266041403e0..7e070b4f386 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -89,7 +89,6 @@ label { @include box-shadow(none); } -.issuable-description, .wiki-content { margin-top: 35px; } diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index fcd62373bfd..71afccba001 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -2,7 +2,14 @@ * Application Header * */ +$header-height: 46px; + header { + &.navbar-empty { + background: #FFF; + border-bottom: 1px solid #EEE; + } + &.navbar-gitlab { z-index: 100; margin-bottom: 0; @@ -11,161 +18,104 @@ header { width: 100%; .container { + background: #FFF; width: 100% !important; padding: 0; - - background: #FFF; - border-bottom: 1px solid #EEE; filter: none; - .title { - position: relative; - float: left; - margin: 0; - margin-left: 25px; - font-size: 18px; - line-height: 44px; - font-weight: bold; - color: #444; - - @include str-truncated(37%); - - a { - color: #444; - &:hover { - text-decoration: underline; - } - } - } - - .app_logo { - border-bottom: 1px solid transparent; - margin-bottom: -1px; - - a { - padding: 5px 8px; - - img { - float: left; - } - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 20px; - font-size: 18px; - line-height: 34px; - font-weight: normal; - } - } - } - .nav > li > a { - color: #666; + color: #888; font-size: 14px; - line-height: 32px; - padding: 6px 10px; + line-height: 19px; + padding: 0; + background-color: #f5f5f5; + margin: 9px 0; + margin-left: 10px; + border-radius: 40px; + height: 26px; + width: 26px; + line-height: 26px; + text-align: center; &:hover, &:focus, &:active { - background: none; + background-color: #EEE; } } - /** NAV block with links and profile **/ - .nav { - float: right; - margin-right: 0; - } - .navbar-toggle { color: #666; margin: 0; border-radius: 0; + position: absolute; + right: 2px; &:hover { background-color: #EEE; } } } - - .turbolink-spinner { - font-size: 20px; - margin-right: 10px; - } - - @media (max-width: $screen-xs-max) { - border-width: 0; - font-size: 18px; - - .title { - @include str-truncated(70%); - } - - .navbar-collapse { - margin-top: 47px; - } - - .navbar-nav { - margin: 5px 0; - - .visible-xs, .visable-sm { - display: table-cell !important; - } - } - - li { - display: table-cell; - width: 1%; - - a { - text-align: center; - font-size: 18px !important; - } - } - } } - /** - * - * Logo holder - * - */ - .app_logo { + .header-logo { + border-bottom: 1px solid transparent; float: left; - margin-right: 9px; + height: $header-height; + width: $sidebar_width; a { float: left; - height: 46px; + height: $header-height; width: 100%; + padding: 5px 8px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 20px; + font-size: 18px; + line-height: 34px; + font-weight: normal; + } img { width: 36px; height: 36px; + float: left; } } + &:hover { background-color: #EEE; } } - .profile-pic { - padding: 0px !important; - width: 46px; - height: 46px; - margin-left: 5px; - img { - width: 46px; - height: 46px; + .header-content { + border-bottom: 1px solid #EEE; + padding-right: 35px; + height: $header-height; + + .title { + position: relative; + float: left; + margin: 0; + margin-left: 35px; + font-size: 18px; + line-height: 44px; + font-weight: bold; + color: #444; + + @include str-truncated(37%); + + a { + color: #444; + &:hover { + text-decoration: underline; + } + } } } - /** - * - * Search box - * - */ .search { margin-right: 10px; margin-left: 10px; @@ -177,6 +127,7 @@ header { } .search-input { + width: 220px; background-image: image-url("icon-search.png"); background-repeat: no-repeat; background-position: 10px; @@ -184,56 +135,74 @@ header { padding: 4px 6px; padding-left: 25px; font-size: 13px; - @include border-radius(3px); - border: 1px solid #DDD; - box-shadow: none; - @include transition(all 0.15s ease-in 0s); background-color: #f5f5f5; + border-color: #f5f5f5; + + &:focus { + @include box-shadow(none); + outline: none; + border-color: #DDD; + background-color: #FFF; + } } } } -.search .search-input { - width: 300px; - &:focus { - width: 330px; - } -} +@mixin collapsed-header { + .header-logo { + width: $sidebar_collapsed_width; -@media (max-width: 1200px) { - .search .search-input { - width: 200px; - &:focus { - width: 230px; + h3 { + display: none; } } -} -@media (max-width: $screen-xs-max) { - #nprogress .spinner { - right: 35px !important; + .header-content { + .title { + margin-left: 30px; + } } } @media (max-width: $screen-md-max) { - .header-collapsed, .header-expanded { - width: 52px; + header .container .title { + max-width: 43%; + } - h3 { - display: none; - } + .header-collapsed, .header-expanded { + @include collapsed-header; } } @media(min-width: $screen-md-max) { .header-collapsed { - width: 52px; - - h3 { - display: none; - } + @include collapsed-header; } .header-expanded { } } + +@media (max-width: $screen-xs-max) { + header .container { + font-size: 18px; + + .title { + max-width: 70%; + } + + .navbar-nav { + margin: 0px; + float: none !important; + + .visible-xs, .visable-sm { + display: table-cell !important; + } + } + + li { + display: table-cell; + width: 1%; + } + } +} diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 08bf6e943d2..c502d953c75 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -39,7 +39,6 @@ &:hover { background: $hover; - border-bottom: 1px solid darken($hover, 10%); } &:last-child { diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index b7f6fac5223..74108c1f086 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -57,7 +57,7 @@ } .container .title { - margin-left: 6px !important; + margin-left: 15px !important; max-width: 70% !important; } } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index 754c5b53020..69bddc6f59e 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -1,6 +1,4 @@ .page-with-sidebar { - background: $background-color; - .sidebar-wrapper { position: fixed; top: 0; @@ -102,13 +100,13 @@ padding-left: 50px; .sidebar-wrapper { - width: 52px; + width: $sidebar_collapsed_width; .nav-sidebar { margin-top: 29px; position: fixed; top: 45px; - width: 52px; + width: $sidebar_collapsed_width; li a { padding-left: 18px; @@ -125,7 +123,21 @@ .collapse-nav a { left: 0px; - width: 52px; + width: $sidebar_collapsed_width; + } + + .sidebar-user { + .username { + display: none; + } + + .avatar { + margin-bottom: 10px; + } + + .logout-holder { + text-align: center; + } } } } @@ -170,3 +182,15 @@ @include expanded-sidebar; } } + +.sidebar-user { + position: absolute; + bottom: 0; + width: 100%; + padding: 10px; + color: #fff; + + .avatar { + margin-top: 5px; + } +} diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index e5590897947..66767cb13cb 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -23,6 +23,13 @@ pre { font-family: $monospace_font; } +code { + &.key-fingerprint { + background: $body-bg; + color: $text-color; + } +} + /** * Wiki typography * diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/generic/zen.scss index 26afc21a6ab..bcb8bbe3134 100644 --- a/app/assets/stylesheets/generic/zen.scss +++ b/app/assets/stylesheets/generic/zen.scss @@ -1,7 +1,7 @@ .zennable { position: relative; - input { + .zen-toggle-comment { display: none; } @@ -26,10 +26,12 @@ } } + // Hide the Enter link when we're in Zen mode input:checked ~ .zen-backdrop .zen-enter-link { display: none; } + // Show the Leave link when we're in Zen mode input:checked ~ .zen-backdrop .zen-leave-link { display: block; position: absolute; @@ -62,6 +64,9 @@ } } + // Make the placeholder text in the standard textarea the same color as the + // background, effectively hiding it + .zen-backdrop textarea::-webkit-input-placeholder { color: white; } @@ -78,6 +83,9 @@ color: white; } + // Make the color of the placeholder text in the Zenned-out textarea darker, + // so it becomes visible + input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { color: #999; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 84361e15481..359f4073e87 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -29,10 +29,6 @@ .commits-feed-holder { float: right; - - .btn { - padding: 4px 12px; - } } li.commit { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3165396a94d..f5ac7bd8805 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -123,38 +123,31 @@ .mr-state-widget { font-size: 13px; - background: #F9F9F9; + background: #FAFAFA; margin-bottom: 20px; color: #666; - border: 1px solid #EEE; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + border: 1px solid #e5e5e5; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); + @include border-radius(3px); .ci_widget { padding: 10px 15px; font-size: 15px; - border-bottom: 1px solid #BBB; - color: #777; - background-color: $background-color; + border-bottom: 1px solid #EEE; &.ci-success { color: $gl-success; - border-color: $gl-success; - background-color: #F1FAF1; } &.ci-pending, &.ci-running { color: $gl-warning; - border-color: $gl-warning; - background-color: #FAF5F1; } &.ci-failed, &.ci-canceled, &.ci-error { color: $gl-danger; - border-color: $gl-danger; - background-color: #FAF1F1; } } @@ -162,7 +155,8 @@ padding: 10px 15px; h4 { - font-weight: normal; + font-weight: bold; + margin: 5px 0; } p:last-child { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 589a43c4264..42b8ecabb38 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -33,7 +33,16 @@ ul.notes { &:before { content: "\00b7"; } + font-size: 13px; + + a { + @extend .cgray; + + &:hover { + text-decoration: underline; + } + } } .author { color: #333; @@ -44,6 +53,14 @@ ul.notes { } .author-username { } + + .note-role { + float: right; + margin-top: 1px; + border: 1px solid #bbb; + background-color: transparent; + color: #999; + } } .discussion { @@ -62,11 +79,11 @@ ul.notes { word-wrap: break-word; @include md-typography; - // Reduce left padding of first ul element + // Reduce left padding of first task list ul element ul.task-list:first-child { padding-left: 10px; - // sub-lists should be padded normally + // sub-tasks should be padded normally ul { padding-left: 20px; } @@ -134,28 +151,23 @@ ul.notes { .discussion, .note { - &.note:hover { - .note-actions { display: block; } - } - .discussion-header:hover { - .discussion-actions { display: block; } - } - .discussion-actions, .note-actions { - display: none; float: right; - - i.fa { - font-size: 16px; - line-height: 16px; - vertical-align: middle; - } + margin-left: 10px; a { - @extend .cgray; + margin-left: 5px; + + color: #999; + + i.fa { + font-size: 16px; + line-height: 16px; + } &:hover { + @extend .cgray; &.danger { @extend .cred; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 280e8b57174..5a5fbc468a3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -84,8 +84,9 @@ } .btn { - line-height: 36px; - height: 56px; + line-height: 40px; + height: 42px; + padding: 0px 12px; img { width: 32px; @@ -93,3 +94,17 @@ } } } + +// Profile > Account > Two Factor Authentication +.two-factor-new { + .manual-instructions { + h3 { + margin-top: 0; + } + + // Slightly increase the size of the details so they're easier to read + dl { + font-size: 1.1em; + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 5a8d4665294..b93ea0f020e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,14 +48,16 @@ } .project-home-desc { + color: $gray; + float: left; font-size: 16px; line-height: 1.3; margin-right: 250px; - } - .project-home-desc { - float: left; - color: $gray; + // Render Markdown-generated HTML inline for this block + p { + display: inline; + } } } @@ -129,7 +131,7 @@ } .option-descr { - margin-left: 24px; + margin-left: 36px; color: $gray; } } @@ -209,23 +211,34 @@ ul.nav.nav-projects-tabs { line-height: 1.5; } - .well { - padding: 14px; + .panel { + @include border-radius(3px); - h4 { + .panel-heading, .panel-footer { font-weight: normal; - margin: 0; - color: #555; + background-color: transparent; + color: #666; + border-color: #EEE; + } + + .actions { + margin-top: 10px; } .nav-pills a { padding: 10px; + font-weight: bold; + color: $gl-link-color; } .nav { - margin: 10px 0; + margin-bottom: 15px; } } + + .ci-status-image { + max-height: 22px; + } } .transfer-project .select2-container { @@ -249,7 +262,8 @@ ul.nav.nav-projects-tabs { } .breadcrumb.repo-breadcrumb { - padding: 2px 0; + padding: 0; + line-height: 34px; background: white; border: none; font-size: 16px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 57f63b52aa1..34ee4d7b31e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -106,17 +106,9 @@ } } -.tree-download-holder .btn { - padding: 4px 12px; -} - .tree-ref-holder { float: left; margin-right: 15px; - - .select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice { - padding: 4px 12px; - } } .readme-holder { diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss index 139b3cc1ac4..1b06b4aa925 100644 --- a/app/assets/stylesheets/themes/gitlab-theme.scss +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -1,8 +1,9 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { header { &.navbar-gitlab { - .app_logo { + .header-logo { background-color: $color-darker; + border-color: $color-darker; a { color: $color-light; @@ -19,8 +20,6 @@ } .page-with-sidebar { - background: $color-darker; - .collapse-nav a { color: #FFF; background: $color; @@ -29,6 +28,12 @@ .sidebar-wrapper { background: $color-darker; border-right: 1px solid $color-darker; + + .sidebar-user { + a { + color: $color-light; + } + } } .nav-sidebar li { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 3975e30835e..a01e2a907d7 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -38,10 +38,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :twitter_sharing_enabled, :sign_in_text, :home_page_url, + :after_sign_out_path, :max_attachment_size, :default_project_visibility, :default_snippet_visibility, :restricted_signup_domains_raw, + :version_check_enabled, + :user_oauth_applications, restricted_visibility_levels: [], ) end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index c301e61d1c7..285e8495342 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,13 +1,8 @@ class Admin::DeployKeysController < Admin::ApplicationController before_action :deploy_keys, only: [:index] - before_action :deploy_key, only: [:show, :destroy] + before_action :deploy_key, only: [:destroy] def index - - end - - def show - end def new diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 2dfae13ac5c..4d3e48f7f81 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -47,7 +47,7 @@ class Admin::GroupsController < Admin::ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to admin_groups_path, notice: 'Group was successfully deleted.' end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 0a463239d74..690096bdbcf 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -33,7 +33,7 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') redirect_to :back end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d36e359934c..06d6d61e907 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -86,11 +86,7 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - # 1. Remove groups where user is the only owner - user.solo_owned_groups.map(&:destroy) - - # 2. Remove user with all authored content including personal projects - user.destroy + DeleteUserService.new.execute(user) respond_to do |format| format.html { redirect_to admin_users_path } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eee10d6c22a..62d46a5482e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -89,7 +89,7 @@ class ApplicationController < ActionController::Base end def after_sign_out_path_for(resource) - new_user_session_path + current_application_settings.after_sign_out_path || new_user_session_path end def abilities @@ -252,7 +252,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.sanitize(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me) } + devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } end def hexdigest(string) @@ -289,14 +289,14 @@ class ApplicationController < ActionController::Base def get_issues_collection set_filters_params - issues = IssuesFinder.new.execute(current_user, @filter_params) - issues + @issuable_finder = IssuesFinder.new(current_user, @filter_params) + @issuable_finder.execute end def get_merge_requests_collection set_filters_params - merge_requests = MergeRequestsFinder.new.execute(current_user, @filter_params) - merge_requests + @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) + @issuable_finder.execute end def github_import_enabled? diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb new file mode 100644 index 00000000000..d5918a7af3b --- /dev/null +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -0,0 +1,30 @@ +# == AuthenticatesWithTwoFactor +# +# Controller concern to handle two-factor authentication +# +# Upon inclusion, skips `require_no_authentication` on `:create`. +module AuthenticatesWithTwoFactor + extend ActiveSupport::Concern + + included do + # This action comes from DeviseController, but because we call `sign_in` + # manually, not skipping this action would cause a "You are already signed + # in." error message to be shown upon successful login. + skip_before_action :require_no_authentication, only: [:create] + end + + # Store the user's ID in the session for later retrieval and render the + # two factor code prompt + # + # The user must have been authenticated with a valid login and password + # before calling this method! + # + # user - User record + # + # Returns nil + def prompt_for_two_factor(user) + session[:otp_user_id] = user.id + + render 'devise/sessions/two_factor' and return + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index a11c554a2af..040255f08e6 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -66,7 +66,11 @@ class Groups::GroupMembersController < Groups::ApplicationController @group_member.destroy redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else - return render_403 + if @group.last_owner?(current_user) + redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.") + else + return render_403 + end end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 34f0b257db3..2e381822e42 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,7 +82,7 @@ class GroupsController < Groups::ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to root_path, notice: 'Group was removed.' end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 507b8290a2b..fc31118124b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,6 +1,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController + include Gitlab::CurrentSettings include PageLayoutHelper + before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! layout 'profile' @@ -32,6 +34,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController private + def verify_user_oauth_applications_enabled + return if current_application_settings.user_oauth_applications? + + redirect_to applications_profile_url + end + def set_application @application = current_user.oauth_applications.find(params[:id]) end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index bb9d65c9ed6..a767815b311 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + + protect_from_forgery except: [:kerberos, :saml] + Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do handle_omniauth @@ -65,8 +68,15 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return end end - rescue Gitlab::OAuth::ForbiddenAction => e - flash[:notice] = e.message + rescue Gitlab::OAuth::SignupDisabledError => e + message = "Signing in using your #{oauth['provider']} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{oauth['provider']} account." + end + + flash[:notice] = message + redirect_to new_user_session_path end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index dcbbe5baa4b..145f27b67dd 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -15,4 +15,45 @@ class PasswordsController < Devise::PasswordsController respond_with(resource) end end + + # After a user resets their password, prompt for 2FA code if enabled instead + # of signing in automatically + # + # See http://git.io/vURrI + def update + super do |resource| + # TODO (rspeicher): In Devise master (> 3.4.1), we can set + # `Devise.sign_in_after_reset_password = false` and avoid this mess. + if resource.errors.empty? && resource.try(:otp_required_for_login?) + resource.unlock_access! if unlockable?(resource) + + # Since we are not signing this user in, we use the :updated_not_active + # message which only contains "Your password was changed successfully." + set_flash_message(:notice, :updated_not_active) if is_flashing_format? + + # Redirect to sign in so they can enter 2FA code + respond_with(resource, location: new_session_path(resource)) and return + end + end + end + + def edit + super + reset_password_token = Devise.token_generator.digest( + User, + :reset_password_token, + resource.reset_password_token + ) + + unless reset_password_token.nil? + user = User.where( + reset_password_token: reset_password_token + ).first_or_initialize + + unless user.reset_password_period_valid? + flash[:alert] = 'Your password reset token has expired.' + redirect_to(new_user_password_url(user_email: user['email'])) + end + end + end end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb new file mode 100644 index 00000000000..42579b3eb44 --- /dev/null +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -0,0 +1,50 @@ +class Profiles::TwoFactorAuthsController < Profiles::ApplicationController + def new + unless current_user.otp_secret + current_user.otp_secret = User.generate_otp_secret(32) + current_user.save! + end + + @qr_code = build_qr_code + end + + def create + if current_user.valid_otp?(params[:pin_code]) + current_user.otp_required_for_login = true + @codes = current_user.generate_otp_backup_codes! + current_user.save! + + render 'create' + else + @error = 'Invalid pin code' + @qr_code = build_qr_code + + render 'new' + end + end + + def codes + @codes = current_user.generate_otp_backup_codes! + current_user.save! + end + + def destroy + current_user.update_attributes({ + otp_required_for_login: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_backup_codes: nil + }) + + redirect_to profile_account_path + end + + private + + def build_qr_code + issuer = "GitLab | #{current_user.email}" + uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) + end +end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7c20b81c0b1..c5f085c236f 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -6,11 +6,12 @@ class Projects::CompareController < Projects::ApplicationController before_action :authorize_download_code! def index + @ref = Addressable::URI.unescape(params[:to]) end def show base_ref = Addressable::URI.unescape(params[:from]) - head_ref = Addressable::URI.unescape(params[:to]) + @ref = head_ref = Addressable::URI.unescape(params[:to]) compare_result = CompareService.new.execute( current_user, diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 8c1bbf76917..40e2b37912b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -18,10 +18,6 @@ class Projects::DeployKeysController < Projects::ApplicationController @available_public_keys -= @available_project_keys end - def show - @key = @project.deploy_keys.find(params[:id]) - end - def new @key = @project.deploy_keys.new diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 57fc48ac7da..76062446c92 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -53,6 +53,6 @@ class Projects::HooksController < Projects::ApplicationController end def hook_params - params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events) + params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c524e1a0ea3..7d168aa827b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -19,7 +19,15 @@ class Projects::IssuesController < Projects::ApplicationController def index terms = params['issue_search'] @issues = get_issues_collection - @issues = @issues.full_search(terms) if terms.present? + + if terms.present? + if terms =~ /\A#(\d+)\z/ + @issues = @issues.where(iid: $1) + else + @issues = @issues.full_search(terms) + end + end + @issues = @issues.page(params[:page]).per(PER_PAGE) respond_to do |format| diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5b93e95866a..71d3051ab88 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,10 +2,13 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled - before_action :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :toggle_subscription] - before_action :closes_issues, only: [:edit, :update, :show, :diffs] - before_action :validates_merge_request, only: [:show, :diffs] - before_action :define_show_vars, only: [:show, :diffs] + before_action :merge_request, only: [ + :edit, :update, :show, :diffs, :commits, :automerge, :automerge_check, + :ci_status, :toggle_subscription + ] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] + before_action :validates_merge_request, only: [:show, :diffs, :commits] + before_action :define_show_vars, only: [:show, :diffs, :commits] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -19,7 +22,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index terms = params['issue_search'] @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.full_search(terms) if terms.present? + + if terms.present? + if terms =~ /\A[#!](\d+)\z/ + @merge_requests = @merge_requests.where(iid: $1) + else + @merge_requests = @merge_requests.full_search(terms) + end + end + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) respond_to do |format| @@ -59,6 +70,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def commits + render 'show' + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d7fbc979067..b110de11013 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -73,10 +73,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave + if @project.namespace == current_user.namespace + return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.') + end + @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| - format.html { redirect_to :back } + format.html { redirect_to dashboard_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index dc430351551..4ca5fc65459 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,18 +97,15 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute + flash[:alert] = 'Project deleted.' - respond_to do |format| - format.html do - flash[:alert] = 'Project deleted.' - - if request.referer.include?('/admin') - redirect_to admin_namespaces_projects_path - else - redirect_to dashboard_path - end - end + if request.referer.include?('/admin') + redirect_to admin_namespaces_projects_path + else + redirect_to dashboard_path end + rescue Projects::DestroyService::DestroyError => ex + redirect_to edit_project_path(@project), alert: ex.message end def autocomplete_sources diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 830751a989f..6ccc7934f2f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - current_user.destroy + DeleteUserService.new.execute(current_user) respond_to do |format| format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3f11d7afe6f..4d976fe6630 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,4 +1,9 @@ class SessionsController < Devise::SessionsController + include AuthenticatesWithTwoFactor + + prepend_before_action :authenticate_with_two_factor, only: [:create] + before_action :auto_sign_in_with_provider, only: [:new] + def new redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes') @@ -14,7 +19,7 @@ class SessionsController < Devise::SessionsController # Prevent a 'you are already signed in' message directly after signing: # we should never redirect to '/users/sign_in' after signing in successfully. - unless redirect_path == '/users/sign_in' + unless redirect_path == new_user_session_path store_location_for(:redirect, redirect_path) end @@ -27,11 +32,67 @@ class SessionsController < Devise::SessionsController def create super do |resource| - # User has successfully signed in, so clear any unused reset tokens + # User has successfully signed in, so clear any unused reset token if resource.reset_password_token.present? resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end end end + + private + + def user_params + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + end + + def find_user + if user_params[:login] + User.by_login(user_params[:login]) + elsif user_params[:otp_attempt] && session[:otp_user_id] + User.find(session[:otp_user_id]) + end + end + + def authenticate_with_two_factor + user = self.resource = find_user + + return unless user && user.otp_required_for_login + + if user_params[:otp_attempt].present? && session[:otp_user_id] + if valid_otp_attempt?(user) + # Remove any lingering user data from login + session.delete(:otp_user_id) + + sign_in(user) and return + else + flash.now[:alert] = 'Invalid two-factor code.' + render :two_factor and return + end + else + if user && user.valid_password?(user_params[:password]) + prompt_for_two_factor(user) + end + end + end + + def auto_sign_in_with_provider + provider = Gitlab.config.omniauth.auto_sign_in_with_provider + return unless provider.present? + + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # to do nothing to prevent redirection loops with certain Omniauth providers. + return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') + + # Prevent alert from popping up on the first page shown after authentication. + flash[:alert] = nil + + redirect_to omniauth_authorize_path(:user, provider.to_sym) + end + + def valid_otp_attempt?(user) + user.valid_otp?(user_params[:otp_attempt]) || + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 17edff68be2..28536e359e5 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -52,13 +52,13 @@ class UploadsController < ApplicationController def upload_model upload_models = { - user: User, - project: Project, - note: Note, - group: Group + "user" => User, + "project" => Project, + "note" => Note, + "group" => Group } - upload_models[params[:model].to_sym] + upload_models[params[:model]] end def upload_mount diff --git a/app/finders/README.md b/app/finders/README.md index 1f46518d230..1a1c69dea38 100644 --- a/app/finders/README.md +++ b/app/finders/README.md @@ -16,7 +16,7 @@ issues = project.issues_for_user_filtered_by(user, params) Better use this: ```ruby -issues = IssuesFinder.new.execute(project, user, filter) +issues = IssuesFinder.new(project, user, filter).execute ``` It will help keep models thiner. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b8f367c6339..0bed2115dc7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -23,10 +23,12 @@ class IssuableFinder attr_accessor :current_user, :params - def execute(current_user, params) + def initialize(current_user, params) @current_user = current_user @params = params + end + def execute items = init_collection items = by_scope(items) items = by_state(items) @@ -40,6 +42,77 @@ class IssuableFinder items = sort(items) end + def group + return @group if defined?(@group) + + @group = + if params[:group_id].present? + Group.find(params[:group_id]) + else + nil + end + end + + def project + return @project if defined?(@project) + + @project = + if params[:project_id].present? + Project.find(params[:project_id]) + else + nil + end + end + + def search + params[:search].presence + end + + def milestones? + params[:milestone_title].present? + end + + def milestones + return @milestones if defined?(@milestones) + + @milestones = + if milestones? && params[:milestone_title] != NONE + Milestone.where(title: params[:milestone_title]) + else + nil + end + end + + def assignee? + params[:assignee_id].present? + end + + def assignee + return @assignee if defined?(@assignee) + + @assignee = + if assignee? && params[:assignee_id] != NONE + User.find(params[:assignee_id]) + else + nil + end + end + + def author? + params[:author_id].present? + end + + def author + return @author if defined?(@author) + + @author = + if author? && params[:author_id] != NONE + User.find(params[:author_id]) + else + nil + end + end + private def init_collection @@ -75,6 +148,10 @@ class IssuableFinder case params[:state] when 'closed' items.closed + when 'rejected' + items.respond_to?(:rejected) ? items.rejected : items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed when 'all' items when 'opened' @@ -85,25 +162,19 @@ class IssuableFinder end def by_group(items) - if params[:group_id].present? - items = items.of_group(Group.find(params[:group_id])) - end + items = items.of_group(group) if group items end def by_project(items) - if params[:project_id].present? - items = items.of_projects(params[:project_id]) - end + items = items.of_projects(project.id) if project items end def by_search(items) - if params[:search].present? - items = items.search(params[:search]) - end + items = items.search(search) if search items end @@ -113,25 +184,24 @@ class IssuableFinder end def by_milestone(items) - if params[:milestone_title].present? - milestone_ids = (params[:milestone_title] == NONE ? nil : Milestone.where(title: params[:milestone_title]).pluck(:id)) - items = items.where(milestone_id: milestone_ids) + if milestones? + items = items.where(milestone_id: milestones.try(:pluck, :id)) end items end def by_assignee(items) - if params[:assignee_id].present? - items = items.where(assignee_id: (params[:assignee_id] == NONE ? nil : params[:assignee_id])) + if assignee? + items = items.where(assignee_id: assignee.try(:id)) end items end def by_author(items) - if params[:author_id].present? - items = items.where(author_id: (params[:author_id] == NONE ? nil : params[:author_id])) + if author? + items = items.where(author_id: author.try(:id)) end items @@ -151,10 +221,6 @@ class IssuableFinder items end - def project - Project.where(id: params[:project_id]).first if params[:project_id].present? - end - def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6e86400a4f6..a539ec49f7a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -214,7 +214,7 @@ module ApplicationHelper def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago') capture_haml do haml_tag :time, date.to_s, - class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'), + class: html_class, datetime: date.getutc.iso8601, title: date.in_time_zone.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement } haml_tag :script, "$('." + html_class + "').timeago().tooltip()" @@ -222,18 +222,28 @@ module ApplicationHelper end def render_markup(file_name, file_content) - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + if gitlab_markdown?(file_name) + Haml::Helpers.preserve(markdown(file_content)) + elsif asciidoc?(file_name) + asciidoc(file_content) + else + GitHub::Markup.render(file_name, file_content). + force_encoding(file_content.encoding).html_safe + end rescue RuntimeError simple_format(file_content) end def markup?(filename) - Gitlab::MarkdownHelper.markup?(filename) + Gitlab::MarkupHelper.markup?(filename) end def gitlab_markdown?(filename) - Gitlab::MarkdownHelper.gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) + end + + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) end # Overrides ActionView::Helpers::UrlHelper#link_to to add `rel="nofollow"` to @@ -269,10 +279,6 @@ module ApplicationHelper html_options end - def escaped_autolink(text) - auto_link ERB::Util.html_escape(text), link: :urls - end - def promo_host 'about.gitlab.com' end @@ -320,16 +326,29 @@ module ApplicationHelper end def state_filters_text_for(entity, project) - entity_title = entity.to_s.humanize + titles = { + opened: "Open", + merged: "Accepted" + } + + entity_title = titles[entity] || entity.to_s.humanize count = if project.nil? - "" + nil elsif current_controller?(:issues) - " (#{project.issues.send(entity).count})" + project.issues.send(entity).count elsif current_controller?(:merge_requests) - " (#{project.merge_requests.send(entity).count})" + project.merge_requests.send(entity).count end - "#{entity_title}#{count}" + + html = content_tag :span, entity_title + + if count.present? + html += " " + html += content_tag :span, number_with_delimiter(count), class: 'badge' + end + + html.html_safe end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 241d6075c9f..63c3ff5674d 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -19,6 +19,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def user_oauth_applications? + current_application_settings.user_oauth_applications + end + # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. def restricted_level_checkboxes(help_block_id) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 4ea838ca447..50df3801703 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,6 @@ module BlobHelper - def highlight(blob_name, blob_content, nowrap = false) - formatter = Rugments::Formatters::HTML.new( + def highlight(blob_name, blob_content, nowrap: false, continue: false) + @formatter ||= Rugments::Formatters::HTML.new( nowrap: nowrap, cssclass: 'code highlight', lineanchors: true, @@ -8,12 +8,14 @@ module BlobHelper ) begin - lexer = Rugments::Lexer.guess(filename: blob_name, source: blob_content) - rescue Rugments::Lexer::AmbiguousGuess + @lexer ||= Rugments::Lexer.guess(filename: blob_name, source: blob_content).new + result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + rescue lexer = Rugments::Lexers::PlainText + result = @formatter.format(lexer.lex(blob_content)).html_safe end - formatter.format(lexer.lex(blob_content)).html_safe + result end def no_highlight_files @@ -55,7 +57,7 @@ module BlobHelper end def editing_preview_title(filename) - if Gitlab::MarkdownHelper.previewable?(filename) + if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else 'Preview changes' diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 01847c6b807..f1dc906cab4 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -1,21 +1,20 @@ module CompareHelper - def compare_to_mr_button? - @project.merge_requests_enabled && - params[:from].present? && - params[:to].present? && - @repository.branch_names.include?(params[:from]) && - @repository.branch_names.include?(params[:to]) && - params[:from] != params[:to] && - !@refs_are_same + def create_mr_button?(from = params[:from], to = params[:to], project = @project) + from.present? && + to.present? && + from != to && + project.merge_requests_enabled && + project.repository.branch_names.include?(from) && + project.repository.branch_names.include?(to) end - def compare_mr_path + def create_mr_path(from = params[:from], to = params[:to], project = @project) new_namespace_project_merge_request_path( - @project.namespace, - @project, + project.namespace, + project, merge_request: { - source_branch: params[:to], - target_branch: params[:from] + source_branch: to, + target_branch: from } ) end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 1b10795bb7b..1bd3ec5e0e0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -101,6 +101,10 @@ module DiffHelper (bottom) ? 'js-unfold-bottom' : '' end + def unfold_class(unfold) + (unfold) ? 'unfold js-unfold' : '' + end + def diff_line_content(line) if line.blank? " " diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 0df3ecc90b7..128de18bc47 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -35,4 +35,23 @@ module EmailsHelper lexer = Rugments::Lexers::Diff.new raw formatter.format(lexer.lex(diffcontent)) end + + def password_reset_token_valid_time + valid_hours = Devise.reset_password_within / 60 / 60 + if valid_hours >= 24 + unit = 'day' + valid_length = (valid_hours / 24).floor + else + unit = 'hour' + valid_length = valid_hours.floor + end + + pluralize(valid_length, unit) + end + + def reset_token_expire_message + link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) + msg = "This link is valid for #{password_reset_token_valid_time}. " + msg << "After it expires, you can #{link_tag}." + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 18c75a8726b..d440da050e1 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -168,8 +168,8 @@ module EventsHelper end end - def event_note(text) - text = first_line_in_markdown(text, 150) + def event_note(text, options = {}) + text = first_line_in_markdown(text, 150, options) sanitize(text, tags: %w(a img b pre code p span)) end @@ -189,7 +189,7 @@ module EventsHelper xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link href: event_link xml.title truncate(event_title, length: 80) - xml.updated event.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.updated event.created_at.strftime("%Y-%m-%dT%H:%M:%S%Z") xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) xml.author do |author| xml.name event.author_name diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 24263a0f619..2777944fc9d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -1,3 +1,5 @@ +require 'nokogiri' + module GitlabMarkdownHelper include Gitlab::Markdown @@ -19,199 +21,82 @@ module GitlabMarkdownHelper escape_once(body) end - gfm_body = gfm(escaped_body, @project, html_options) + gfm_body = gfm(escaped_body, {}, html_options) - gfm_body.gsub!(%r{<a.*?>.*?</a>}m) do |match| - "</a>#{match}#{link_to("", url, html_options)[0..-5]}" # "</a>".length +1 + fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) + if fragment.children.size == 1 && fragment.children[0].name == 'a' + # Fragment has only one node, and it's a link generated by `gfm`. + # Replace it with our requested link. + text = fragment.children[0].text + fragment.children[0].replace(link_to(text, url, html_options)) + else + # Traverse the fragment's first generation of children looking for pure + # text, wrapping anything found in the requested link + fragment.children.each do |node| + next unless node.text? + node.replace(link_to(node.text, url, html_options)) + end end - link_to(gfm_body.html_safe, url, html_options) + fragment.to_html.html_safe end + MARKDOWN_OPTIONS = { + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + footnotes: true + }.freeze + def markdown(text, options={}) unless @markdown && options == @options @options = options # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch - rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, { - # Handled further down the line by Gitlab::Markdown::SanitizationFilter - escape_html: false - }.merge(options)) + rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options) # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @markdown = Redcarpet::Markdown.new(rend, - no_intra_emphasis: true, - tables: true, - fenced_code_blocks: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true, - footnotes: true - ) + @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) end @markdown.render(text).html_safe end + def asciidoc(text) + Gitlab::Asciidoc.render(text, { + commit: @commit, + project: @project, + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref + }) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then # the tag contents are truncated without removing the closing tag. - def first_line_in_markdown(text, max_chars = nil) - md = markdown(text).strip + def first_line_in_markdown(text, max_chars = nil, options = {}) + md = markdown(text, options).strip truncate_visible(md, max_chars || md.length) if md.present? end def render_wiki_content(wiki_page) - if wiki_page.format == :markdown + case wiki_page.format + when :markdown markdown(wiki_page.content) + when :asciidoc + asciidoc(wiki_page.content) else wiki_page.formatted_content.html_safe end end - # TODO (rspeicher): This should be its own filter - def create_relative_links(text) - paths = extract_paths(text) - - paths.uniq.each do |file_path| - # If project does not have repository - # its nothing to rebuild - # - # TODO: pass project variable to markdown helper instead of using - # instance variable. Right now it generates invalid path for pages out - # of project scope. Example: search results where can be rendered markdown - # from different projects - if @repository && @repository.exists? && !@repository.empty? - new_path = rebuild_path(file_path) - # Finds quoted path so we don't replace other mentions of the string - # eg. "doc/api" will be replaced and "/home/doc/api/text" won't - text.gsub!("\"#{file_path}\"", "\"/#{new_path}\"") - end - end - - text - end - - def extract_paths(text) - links = substitute_links(text) - image_links = substitute_image_links(text) - links + image_links - end - - def substitute_links(text) - links = text.scan(/<a href=\"([^"]*)\">/) - relative_links = links.flatten.reject{ |link| link_to_ignore? link } - relative_links - end - - def substitute_image_links(text) - links = text.scan(/<img src=\"([^"]*)\"/) - relative_links = links.flatten.reject{ |link| link_to_ignore? link } - relative_links - end - - def link_to_ignore?(link) - if link =~ /\A\#\w+/ - # ignore anchors like <a href="#my-header"> - true - else - ignored_protocols.map{ |protocol| link.include?(protocol) }.any? - end - end - - def ignored_protocols - ["http://","https://", "ftp://", "mailto:", "smb://"] - end - - def rebuild_path(file_path) - file_path = file_path.dup - file_path.gsub!(/(#.*)/, "") - id = $1 || "" - file_path = relative_file_path(file_path) - file_path = sanitize_slashes(file_path) - - [ - Gitlab.config.gitlab.relative_url_root, - @project.path_with_namespace, - path_with_ref(file_path), - file_path - ].compact.join("/").gsub(/\A\/*|\/*\z/, '') + id - end - - def sanitize_slashes(path) - path[0] = "" if path.start_with?("/") - path.chop if path.end_with?("/") - path - end - - def relative_file_path(path) - requested_path = @path - nested_path = build_nested_path(path, requested_path) - return nested_path if file_exists?(nested_path) - path - end - - # Covering a special case, when the link is referencing file in the same directory eg: - # If we are at doc/api/README.md and the README.md contains relative links like [Users](users.md) - # this takes the request path(doc/api/README.md), and replaces the README.md with users.md so the path looks like doc/api/users.md - # If we are at doc/api and the README.md shown in below the tree view - # this takes the request path(doc/api) and adds users.md so the path looks like doc/api/users.md - def build_nested_path(path, request_path) - return request_path if path == "" - return path unless request_path - if local_path(request_path) == "tree" - base = request_path.split("/").push(path) - base.join("/") - else - base = request_path.split("/") - base.pop - base.push(path).join("/") - end - end - - # Checks if the path exists in the repo - # eg. checks if doc/README.md exists, if not then link to blob - def path_with_ref(path) - if file_exists?(path) - "#{local_path(path)}/#{correct_ref}" - else - "blob/#{correct_ref}" - end - end - - def file_exists?(path) - return false if path.nil? - @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any? - end - - # Check if the path is pointing to a directory(tree) or a file(blob) - # eg. doc/api is directory and doc/README.md is file - def local_path(path) - return "tree" if @repository.tree(current_sha, path).entries.any? - return "raw" if @repository.blob_at(current_sha, path).image? - "blob" - end - - def current_sha - if @commit - @commit.id - elsif @repository && !@repository.empty? - if @ref - @repository.commit(@ref).try(:sha) - else - @repository.head_commit.sha - end - end - end - - # We will assume that if no ref exists we can point to master - def correct_ref - @ref ? @ref : "master" - end - private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -260,15 +145,25 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String def cross_project_reference(project, entity) - path = project.path_with_namespace - - if entity.kind_of?(Issue) - [path, entity.iid].join('#') - elsif entity.kind_of?(MergeRequest) - [path, entity.iid].join('!') + if entity.respond_to?(:to_reference) + "#{project.to_reference}#{entity.to_reference}" else - raise 'Not supported type' + '' end end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a9030729b48..a730684f8f3 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -26,15 +26,15 @@ module IconsHelper end def public_icon - icon('globe') + icon('globe fw') end def internal_icon - icon('shield') + icon('shield fw') end def private_icon - icon('lock') + icon('lock fw') end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 8272c177d59..8036303851b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,6 +1,44 @@ module LabelsHelper include ActionView::Helpers::TagHelper + # Link to a Label + # + # label - Label object to link to + # project - Project object which will be used as the context for the label's + # link. If omitted, defaults to `@project`, or the label's own + # project. + # block - An optional block that will be passed to `link_to`, forming the + # body of the link element. If omitted, defaults to + # `render_colored_label`. + # + # Examples: + # + # # Allow the generated link to use the label's own project + # link_to_label(label) + # + # # Force the generated link to use @project + # @project = Project.first + # link_to_label(label) + # + # # Force the generated link to use a provided project + # link_to_label(label, project: Project.last) + # + # # Customize link body with a block + # link_to_label(label) { "My Custom Label Text" } + # + # Returns a String + def link_to_label(label, project: nil, &block) + project ||= @project || label.project + link = namespace_project_issues_path(project.namespace, project, + label_name: label.name) + + if block_given? + link_to link, &block + else + link_to render_colored_label(label), link + end + end + def project_label_names @project.labels.pluck(:title) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 96d2606f1a1..94ce6646634 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -148,7 +148,7 @@ module ProjectsHelper nav_tabs << [:files, :commits, :network, :graphs] end - if project.repo_exists? && project.merge_requests_enabled + if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end @@ -156,11 +156,19 @@ module ProjectsHelper nav_tabs << :settings end - [:issues, :wiki, :snippets].each do |feature| - nav_tabs << feature if project.send :"#{feature}_enabled" + if can?(current_user, :read_issue, project) + nav_tabs << :issues end - if project.issues_enabled || project.merge_requests_enabled + if can?(current_user, :read_wiki, project) + nav_tabs << :wiki + end + + if can?(current_user, :read_project_snippet, project) + nav_tabs << :snippets + end + + if can?(current_user, :read_milestone, project) nav_tabs << [:milestones, :labels] end @@ -286,4 +294,16 @@ module ProjectsHelper nil end end + + def user_max_access_in_project(user, project) + level = project.team.max_member_access(user) + + if level + Gitlab::Access.options_with_owner.key(level) + end + end + + def leave_project_message(project) + "Are you sure you want to leave \"#{project.name}\" project?" + end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index bec8f2f1aa7..2b99a398049 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -10,6 +10,7 @@ module SelectsHelper any_user = opts[:any_user] || false email_user = opts[:email_user] || false first_user = opts[:first_user] && current_user ? current_user.username : false + project = opts[:project] || @project html = { class: css_class, @@ -21,8 +22,8 @@ module SelectsHelper } unless opts[:scope] == :all - if @project - html['data-project-id'] = @project.id + if project + html['data-project-id'] = project.id elsif @group html['data-group-id'] = @group.id end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6dd9b6f017c..03a49e119b8 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -25,13 +25,7 @@ module TreeHelper end def render_readme(readme) - if gitlab_markdown?(readme.name) - preserve(markdown(readme.data)) - elsif markup?(readme.name) - render_markup(readme.name, readme.data) - else - simple_format(readme.data) - end + render_markup(readme.name, readme.data) end # Return an image icon depending on the file type and mode diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb new file mode 100644 index 00000000000..f64d730b448 --- /dev/null +++ b/app/helpers/version_check_helper.rb @@ -0,0 +1,7 @@ +module VersionCheckHelper + def version_status_badge + if Rails.env.production? + image_tag VersionCheck.new.url + end + end +end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 9cb7077e59d..4a6e18e6a74 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -93,7 +93,8 @@ module Emails "pushed to" end - @subject = "[#{@project.path_with_namespace}]" + @subject = "[Git]" + @subject << "[#{@project.path_with_namespace}]" @subject << "[#{@ref_name}]" if action == :push @subject << " " diff --git a/app/models/ability.rb b/app/models/ability.rb index 85a15596f8d..4e6c60dc8ca 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -101,6 +101,27 @@ class Ability rules -= project_archived_rules end + unless project.issues_enabled + rules -= named_abilities('issue') + end + + unless project.merge_requests_enabled + rules -= named_abilities('merge_request') + end + + unless project.issues_enabled or project.merge_requests_enabled + rules -= named_abilities('label') + rules -= named_abilities('milestone') + end + + unless project.snippets_enabled + rules -= named_abilities('project_snippet') + end + + unless project.wiki_enabled + rules -= named_abilities('wiki') + end + rules end end @@ -272,5 +293,16 @@ class Ability abilities end end + + private + + def named_abilities(name) + [ + :"read_#{name}", + :"write_#{name}", + :"modify_#{name}", + :"admin_#{name}" + ] + end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d5123249c53..80463ee8841 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -18,6 +18,8 @@ # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text +# user_oauth_applications :bool default(TRUE) +# after_sign_out_path :string(255) # class ApplicationSetting < ActiveRecord::Base @@ -30,6 +32,10 @@ class ApplicationSetting < ActiveRecord::Base format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :home_page_url_column_exist + validates :after_sign_out_path, + allow_blank: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| diff --git a/app/models/commit.rb b/app/models/commit.rb index be5a118bfec..f02fe240540 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,9 +1,11 @@ class Commit - include ActiveModel::Conversion - include StaticModel extend ActiveModel::Naming + + include ActiveModel::Conversion include Mentionable include Participable + include Referable + include StaticModel attr_mentionable :safe_message participant :author, :committer, :notes, :mentioned_users @@ -56,6 +58,34 @@ class Commit @raw.id end + def ==(other) + (self.class === other) && (raw == other.raw) + end + + def self.reference_prefix + '@' + end + + # Pattern used to extract commit references from text + # + # The SHA can be between 6 and 40 hex characters. + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?<commit>\h{6,40}) + }x + end + + def to_reference(from_project = nil) + if cross_project_reference?(from_project) + "#{project.to_reference}@#{id}" + else + id + end + end + def diff_line_count @diff_line_count ||= Commit::diff_line_count(self.diffs) @diff_line_count @@ -126,11 +156,6 @@ class Commit Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end - # Mentionable override. - def gfm_reference - "commit #{id}" - end - def author User.find_for_commit(author_email, author_name) end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index e6456198264..86fc9eb01a3 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -19,6 +19,7 @@ # class CommitRange include ActiveModel::Conversion + include Referable attr_reader :sha_from, :notation, :sha_to @@ -28,10 +29,24 @@ class CommitRange # See `exclude_start?` attr_reader :exclude_start - # The beginning and ending SHA sums can be between 6 and 40 hex characters, - # and the range selection can be double- or triple-dot. + # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # the range notation can be double- or triple-dot. PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + def self.reference_prefix + '@' + end + + # Pattern used to extract commit range references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (?:#{Project.reference_pattern}#{reference_prefix})? + (?<commit_range>#{PATTERN}) + }x + end + # Initialize a CommitRange # # range_string - The String commit range. @@ -59,6 +74,17 @@ class CommitRange "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" end + def to_reference(from_project = nil) + # Not using to_s because we want the full SHAs + reference = sha_from + notation + sha_to + + if cross_project_reference?(from_project) + reference = project.to_reference + '@' + reference + end + + reference + end + # Returns a String for use in a link's title attribute def reference_title "Commits #{suffixed_sha_from} through #{sha_to}" diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 3ef3e8b67d8..6f9f54d08cc 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,10 +20,15 @@ module Mentionable end end - # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must - # be overridden if this model object can be referenced directly by GFM notation. - def gfm_reference - raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") + # Returns the text used as the body of a Note when this object is referenced + # + # By default this will be the class name and the result of calling + # `to_reference` on the object. + def gfm_reference(from_project = nil) + # "MergeRequest" > "merge_request" > "Merge request" > "merge request" + friendly_name = self.class.to_s.underscore.humanize.downcase + + "#{friendly_name} #{to_reference(from_project)}" end # Construct a String that contains possible GFM references. @@ -39,13 +44,13 @@ module Mentionable # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def has_mentioned?(target) - Note.cross_reference_exists?(target, local_reference) + SystemNoteService.cross_reference_exists?(target, local_reference) end - def mentioned_users(current_user = nil, p = project) + def mentioned_users(current_user = nil) return [] if mentionable_text.blank? - ext = Gitlab::ReferenceExtractor.new(p, current_user) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user) ext.analyze(mentionable_text) ext.users.uniq end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 7a5e4876ff2..9f667f47e0d 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -35,31 +35,39 @@ module Participable end end - def participants(current_user = self.author) - self.class.participant_attrs.flat_map do |attr| + def participants(current_user = self.author, project = self.project) + participants = self.class.participant_attrs.flat_map do |attr| meth = method(attr) value = - if meth.arity == 1 + if meth.arity == 1 || meth.arity == -1 meth.call(current_user) else meth.call end - participants_for(value, current_user) + participants_for(value, current_user, project) end.compact.uniq + + if project + participants.select! do |user| + user.can?(:read_project, project) + end + end + + participants end private - def participants_for(value, current_user = nil) + def participants_for(value, current_user = nil, project = nil) case value when User [value] when Enumerable, ActiveRecord::Relation - value.flat_map { |v| participants_for(v, current_user) } + value.flat_map { |v| participants_for(v, current_user, project) } when Participable - value.participants(current_user) + value.participants(current_user, project) end end end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb new file mode 100644 index 00000000000..cced66cc1e4 --- /dev/null +++ b/app/models/concerns/referable.rb @@ -0,0 +1,61 @@ +# == Referable concern +# +# Contains functionality related to making a model referable in Markdown, such +# as "#1", "!2", "~3", etc. +module Referable + extend ActiveSupport::Concern + + # Returns the String necessary to reference this object in Markdown + # + # from_project - Refering Project object + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.first.to_reference # => "#1" + # Issue.last.to_reference(other_project) # => "cross-project#1" + # + # Returns a String + def to_reference(_from_project = nil) + '' + end + + module ClassMethods + # The character that prefixes the actual reference identifier + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.reference_prefix # => '#' + # MergeRequest.reference_prefix # => '!' + # + # Returns a String + def reference_prefix + '' + end + + # Regexp pattern used to match references to this object + # + # This must be overridden by the including class. + # + # Returns a Regexp + def reference_pattern + raise NotImplementedError, "#{self} does not implement #{__method__}" + end + end + + private + + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + if self.is_a?(Project) + self != from_project + else + from_project && self.project && self.project != from_project + end + end +end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 85fdb12bfdc..49f6c95e045 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -1,4 +1,6 @@ class ExternalIssue + include Referable + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -26,4 +28,13 @@ class ExternalIssue def project @project end + + # Pattern used to extract `JIRA-123` issue references from text + def self.reference_pattern + %r{(?<issue>([A-Z\-]+-)\d+)} + end + + def to_reference(_from_project = nil) + id + end end diff --git a/app/models/group.rb b/app/models/group.rb index 1386a9eccc9..051c672cb33 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -17,10 +17,12 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Group < Namespace + include Referable + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :users, through: :group_members - validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader @@ -36,6 +38,18 @@ class Group < Namespace def sort(method) order_by(method) end + + def reference_prefix + User.reference_prefix + end + + def reference_pattern + User.reference_pattern + end + end + + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{name}" end def human_name @@ -87,10 +101,14 @@ class Group < Namespace end def post_create_hook + Gitlab::AppLogger.info("Group \"#{name}\" was created") + system_hook_service.execute_hooks_for(self, :create) end def post_destroy_hook + Gitlab::AppLogger.info("Group \"#{name}\" was removed") + system_hook_service.execute_hooks_for(self, :destroy) end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 7e4f16ebf16..ab055f6b80b 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -44,7 +44,7 @@ class GroupMilestone def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def state diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 21867a9316c..ca7066b959a 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class ProjectHook < WebHook @@ -21,5 +22,6 @@ class ProjectHook < WebHook scope :push_hooks, -> { where(push_events: true) } scope :tag_push_hooks, -> { where(tag_push_events: true) } scope :issue_hooks, -> { where(issues_events: true) } + scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } end diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 2e11239c40b..b55e217975f 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -13,8 +13,13 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class ServiceHook < WebHook belongs_to :service + + def execute(data) + super(data, 'service_hook') + end end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index ee32b49bc66..6fb2d421026 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 315d96af1b9..46fb85336e5 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # class WebHook < ActiveRecord::Base @@ -21,6 +22,7 @@ class WebHook < ActiveRecord::Base default_value_for :push_events, true default_value_for :issues_events, false + default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false @@ -30,12 +32,15 @@ class WebHook < ActiveRecord::Base validates :url, presence: true, format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } - def execute(data) + def execute(data, hook_name) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? WebHook.post(url, body: data.to_json, - headers: { "Content-Type" => "application/json" }, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, verify: false) else post_url = url.gsub("#{parsed_url.userinfo}@", "") @@ -45,7 +50,10 @@ class WebHook < ActiveRecord::Base } WebHook.post(post_url, body: data.to_json, - headers: { "Content-Type" => "application/json" }, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, verify: false, basic_auth: auth) end @@ -54,7 +62,7 @@ class WebHook < ActiveRecord::Base false end - def async_execute(data) - Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data) + def async_execute(data, hook_name) + Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e102051387..2456b7d0dc1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -21,10 +21,11 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Issue < ActiveRecord::Base - include Issuable include InternalId - include Taskable + include Issuable + include Referable include Sortable + include Taskable ActsAsTaggableOn.strict_case_match = true @@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base attributes end - # Mentionable overrides. + def self.reference_prefix + '#' + end + + # Pattern used to extract `#123` issue references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<issue>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end - def gfm_reference - "issue ##{iid}" + reference end # Reset issue events cache diff --git a/app/models/label.rb b/app/models/label.rb index eee28acefc1..230631b5180 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -11,6 +11,8 @@ # class Label < ActiveRecord::Base + include Referable + DEFAULT_COLOR = '#428BCA' default_value_for :color, DEFAULT_COLOR @@ -34,6 +36,45 @@ class Label < ActiveRecord::Base alias_attribute :name, :title + def self.reference_prefix + '~' + end + + # Pattern used to extract label references from text + def self.reference_pattern + %r{ + #{reference_prefix} + (?: + (?<label_id>\d+) | # Integer-based label ID, or + (?<label_name> + [A-Za-z0-9_-]+ | # String-based single-word label title, or + "[^&\?,]+" # String-based multi-word label surrounded in quotes + ) + ) + }x + end + + # Returns the String necessary to reference this Label in Markdown + # + # format - Symbol format to use (default: :id, optional: :name) + # + # Note that its argument differs from other objects implementing Referable. If + # a non-Symbol argument is given (such as a Project), it will default to :id. + # + # Examples: + # + # Label.first.to_reference # => "~1" + # Label.first.to_reference(:name) # => "~\"bug\"" + # + # Returns a String + def to_reference(format = :id) + if format == :name && !name.include?('"') + %(#{self.class.reference_prefix}"#{name}") + else + "#{self.class.reference_prefix}#{id}" + end + end + def open_issues_count issues.opened.count end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64f3c39f131..f1f9f23b12c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -25,10 +25,11 @@ require Rails.root.join("app/models/commit") require Rails.root.join("lib/static_model") class MergeRequest < ActiveRecord::Base - include Issuable - include Taskable include InternalId + include Issuable + include Referable include Sortable + include Taskable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -133,7 +134,31 @@ class MergeRequest < ActiveRecord::Base # Closed scope for merge request should return # both merged and closed mr's scope :closed, -> { with_states(:closed, :merged) } - scope :declined, -> { with_states(:closed) } + scope :rejected, -> { with_states(:closed) } + + def self.reference_prefix + '!' + end + + # Pattern used to extract `!123` merge request references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<merge_request>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end def validate_branches if target_project == source_project && target_branch == source_branch @@ -172,7 +197,6 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? reload_code - mark_as_unchecked end end @@ -289,11 +313,6 @@ class MergeRequest < ActiveRecord::Base end end - # Mentionable override. - def gfm_reference - "merge request !#{iid}" - end - def target_project_path if target_project target_project.path_with_namespace diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 9bbb2bafb98..9c543b37023 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -66,7 +66,7 @@ class Milestone < ActiveRecord::Base def percent_complete ((closed_items_count * 100) / total_items_count).abs rescue ZeroDivisionError - 100 + 0 end def expires_at diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 211dfa76b81..03d2ab165ea 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -72,7 +72,7 @@ class Namespace < ActiveRecord::Base path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") # Users with the great usernames of "." or ".." would end up with a blank username. - # Work around that by setting their username to "blank", followed by a counter. + # Work around that by setting their username to "blank", followed by a counter. path = "blank" if path.blank? counter = 0 @@ -99,7 +99,18 @@ class Namespace < ActiveRecord::Base end def rm_dir - gitlab_shell.rm_namespace(path) + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(path, new_path) + message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + Gitlab::AppLogger.info message + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path) + end end def move_dir diff --git a/app/models/note.rb b/app/models/note.rb index cbce6786683..d5f716b3de0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -63,143 +63,9 @@ class Note < ActiveRecord::Base after_update :set_references class << self - def create_status_change_note(noteable, project, author, status, source) - body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}" - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - # +noteable+ was referenced from +mentioner+, by including GFM in either - # +mentioner+'s description or an associated Note. - # Create a system Note associated with +noteable+ with a GFM back-reference - # to +mentioner+. - def create_cross_reference_note(noteable, mentioner, author) - gfm_reference = mentioner_gfm_ref(noteable, mentioner) - - note_options = { - project: noteable.project, - author: author, - note: cross_reference_note_content(gfm_reference), - system: true - } - - if noteable.kind_of?(Commit) - note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) - else - note_options.merge!(noteable: noteable) - end - - create(note_options) unless cross_reference_disallowed?(noteable, mentioner) - end - - def create_milestone_change_note(noteable, project, author, milestone) - body = if milestone.nil? - 'Milestone removed' - else - "Milestone changed to #{milestone.title}" - end - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - def create_assignee_change_note(noteable, project, author, assignee) - body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" - - create({ - noteable: noteable, - project: project, - author: author, - note: body, - system: true - }) - end - - def create_labels_change_note(noteable, project, author, added_labels, removed_labels) - labels_count = added_labels.count + removed_labels.count - added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ') - removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ') - message = '' - - if added_labels.present? - message << "added #{added_labels}" - end - - if added_labels.present? && removed_labels.present? - message << ' and ' - end - - if removed_labels.present? - message << "removed #{removed_labels}" - end - - message << ' ' << 'label'.pluralize(labels_count) - body = "#{message.capitalize}" - - create( - noteable: noteable, - project: project, - author: author, - note: body, - system: true - ) - end - - def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil) - total_count = new_commits.length + existing_commits.length - commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit') - body = "Added #{commits_text}:\n\n" - - if existing_commits.length > 0 - commit_ids = - if existing_commits.length == 1 - existing_commits.first.short_id - else - if oldrev - "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" - else - "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" - end - end - - commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') - - branch = - if merge_request.for_fork? - "#{merge_request.target_project_namespace}:#{merge_request.target_branch}" - else - merge_request.target_branch - end - - message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`" - body << message - body << "\n" - end - - new_commits.each do |commit| - message = "* #{commit.short_id} - #{commit.title}" - body << message - body << "\n" - end - - create( - noteable: merge_request, - project: project, - author: author, - note: body, - system: true - ) + # TODO (rspeicher): Update usages + def create_cross_reference_note(*args) + SystemNoteService.cross_reference(*args) end def discussions_from_notes(notes) @@ -227,88 +93,19 @@ class Note < ActiveRecord::Base [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end - # Determine if cross reference note should be created. - # eg. mentioning a commit in MR comments which exists inside a MR - # should not create "mentioned in" note. - def cross_reference_disallowed?(noteable, mentioner) - if mentioner.kind_of?(MergeRequest) - mentioner.commits.map(&:id).include? noteable.id - end - end - - # Determine whether or not a cross-reference note already exists. - def cross_reference_exists?(noteable, mentioner) - gfm_reference = mentioner_gfm_ref(noteable, mentioner, true) - notes = if noteable.is_a?(Commit) - where(commit_id: noteable.id, noteable_type: 'Commit') - else - where(noteable_id: noteable.id, noteable_type: noteable.class) - end - - notes.where('note like ?', cross_reference_note_pattern(gfm_reference)). - system.any? - end - def search(query) where("note like :query", query: "%#{query}%") end + end - def cross_reference_note_prefix - 'mentioned in ' - end - - private - - def cross_reference_note_content(gfm_reference) - cross_reference_note_prefix + "#{gfm_reference}" - end - - def cross_reference_note_pattern(gfm_reference) - # Older cross reference notes contained underscores for emphasis - "%" + cross_reference_note_content(gfm_reference) + "%" - end - - # Prepend the mentioner's namespaced project path to the GFM reference for - # cross-project references. For same-project references, return the - # unmodified GFM reference. - def mentioner_gfm_ref(noteable, mentioner, cross_reference = false) - if mentioner.is_a?(Commit) && cross_reference - return mentioner.gfm_reference.sub('commit ', 'commit %') - end - - full_gfm_reference(mentioner.project, noteable.project, mentioner) - end - - # Return the +mentioner+ GFM reference. If the mentioner and noteable - # projects are not the same, add the mentioning project's path to the - # returned value. - def full_gfm_reference(mentioning_project, noteable_project, mentioner) - if mentioning_project == noteable_project - mentioner.gfm_reference - else - if mentioner.is_a?(Commit) - mentioner.gfm_reference.sub( - /(commit )/, - "\\1#{mentioning_project.path_with_namespace}@" - ) - else - mentioner.gfm_reference.sub( - /(issue |merge request )/, - "\\1#{mentioning_project.path_with_namespace}" - ) - end - end - end + def cross_reference? + system && SystemNoteService.cross_reference?(note) end def max_attachment_size current_application_settings.max_attachment_size.megabytes.to_i end - def cross_reference? - note.start_with?(self.class.cross_reference_note_prefix) - end - def find_diff return nil unless noteable && noteable.diffs.present? @@ -449,16 +246,6 @@ class Note < ActiveRecord::Base @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) end - # Returns true if this is a downvote note, - # otherwise false is returned - def downvote? - votable? && (note.start_with?('-1') || - note.start_with?(':-1:') || - note.start_with?(':thumbsdown:') || - note.start_with?(':thumbs_down_sign:') - ) - end - def for_commit? noteable_type == "Commit" end @@ -500,14 +287,18 @@ class Note < ActiveRecord::Base nil end - # Returns true if this is an upvote note, - # otherwise false is returned + DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) + + # Check if the note is a downvote + def downvote? + votable? && note.start_with?(*DOWNVOTES) + end + + UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) + + # Check if the note is an upvote def upvote? - votable? && (note.start_with?('+1') || - note.start_with?(':+1:') || - note.start_with?(':thumbsup:') || - note.start_with?(':thumbs_up_sign:') - ) + votable? && note.start_with?(*UPVOTES) end def superceded?(notes) @@ -535,8 +326,8 @@ class Note < ActiveRecord::Base end # Mentionable override. - def gfm_reference - noteable.gfm_reference + def gfm_reference(from_project = nil) + noteable.gfm_reference(from_project) end # Mentionable override. diff --git a/app/models/project.rb b/app/models/project.rb index e866681aab9..3c9f0dad28b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,11 +33,12 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Project < ActiveRecord::Base - include Sortable + include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::ConfigHelper include Rails.application.routes.url_helpers + include Referable + include Sortable extend Gitlab::ConfigHelper extend Enumerize @@ -144,7 +145,7 @@ class Project < ActiveRecord::Base validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, - if: ->(project) { project.avatar && project.avatar_changed? } + if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader @@ -247,6 +248,11 @@ class Project < ActiveRecord::Base order_by(method) end end + + def reference_pattern + name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + %r{(?<project>#{name_pattern}/#{name_pattern})} + end end def team @@ -305,6 +311,10 @@ class Project < ActiveRecord::Base path end + def to_reference(_from_project = nil) + path_with_namespace + end + def web_url [gitlab_config.url, path_with_namespace].join('/') end @@ -483,7 +493,7 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| - hook.async_execute(data) + hook.async_execute(data, hooks_scope.to_s) end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 949a4d7111b..a9354754686 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -40,6 +40,12 @@ class GitlabCiService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) + ci_yaml_file = ci_yaml_file(data) + + if ci_yaml_file + data.merge!(ci_yaml_file: ci_yaml_file) + end + service_hook.execute(data) end @@ -123,6 +129,14 @@ class GitlabCiService < CiService private + def ci_yaml_file(data) + ref = data[:checkout_sha] + repo = project.repository + commit = repo.commit(ref) + blob = Gitlab::Git::Blob.find(repo, commit.id, ".gitlab-ci.yml") + blob && blob.data + end + def fork_registration_path project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 38cb64f8c48..6761f00183e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -63,7 +63,7 @@ class HipchatService < Service private def gate - options = { api_version: api_version || 'v2' } + options = { api_version: api_version.present? ? api_version : 'v2' } options[:server_url] = server unless server.blank? @gate ||= HipChat::Client.new(token, options) end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 0706a1ca0d1..231973fa543 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -2,7 +2,7 @@ class ProjectWiki include Gitlab::ShellAdapter MARKUPS = { - 'Markdown' => :markdown, + 'Markdown' => :md, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc } unless defined?(MARKUPS) diff --git a/app/models/repository.rb b/app/models/repository.rb index 1b8c74028d9..1ca97017637 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -370,8 +370,55 @@ class Repository @root_ref ||= raw_repository.root_ref end + def commit_file(user, path, content, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + content: content, + path: path + } + + Gitlab::Git::Blob.commit(raw_repository, options) + end + + def remove_file(user, path, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + path: path + } + + Gitlab::Git::Blob.remove(raw_repository, options) + end + private + def user_to_comitter(user) + { + email: user.email, + name: user.name, + time: Time.now + } + end + def cache @cache ||= RepositoryCache.new(path_with_namespace) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index d2af26539b6..3ab9e834c63 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -16,14 +16,16 @@ # class Snippet < ActiveRecord::Base - include Sortable - include Linguist::BlobHelper include Gitlab::VisibilityLevel + include Linguist::BlobHelper include Participable + include Referable + include Sortable default_value_for :visibility_level, Snippet::PRIVATE - belongs_to :author, class_name: "User" + belongs_to :author, class_name: 'User' + belongs_to :project has_many :notes, as: :noteable, dependent: :destroy @@ -50,6 +52,30 @@ class Snippet < ActiveRecord::Base participant :author, :notes + def self.reference_prefix + '$' + end + + # Pattern used to extract `$123` snippet references from text + # + # This pattern supports cross-project references. + def self.reference_pattern + %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?<snippet>\d+) + }x + end + + def to_reference(from_project = nil) + reference = "#{self.class.reference_prefix}#{id}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end + def self.content_types [ ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", diff --git a/app/models/tree.rb b/app/models/tree.rb index f279e896cda..93b3246a668 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,11 +1,11 @@ class Tree - include Gitlab::MarkdownHelper + include Gitlab::MarkupHelper attr_accessor :repository, :sha, :path, :entries def initialize(repository, sha, path = '/') path = '/' if path.blank? - + @repository = repository @sha = sha @path = path @@ -20,7 +20,7 @@ class Tree available_readmes = blobs.select(&:readme?) if available_readmes.count == 0 - return @readme = nil + return @readme = nil end # Take the first previewable readme, or the first available readme, if we diff --git a/app/models/user.rb b/app/models/user.rb index 1cf7cfea974..596dc7ea33a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,6 +50,11 @@ # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) # location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean +# otp_backup_codes :text # public_email :string(255) default(""), not null # @@ -57,11 +62,13 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class User < ActiveRecord::Base - include Sortable - include Gitlab::ConfigHelper - include TokenAuthenticatable extend Gitlab::ConfigHelper + + include Gitlab::ConfigHelper include Gitlab::CurrentSettings + include Referable + include Sortable + include TokenAuthenticatable default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group @@ -70,8 +77,14 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme - devise :database_authenticatable, :lockable, :async, - :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable + devise :two_factor_authenticatable, + otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp + + devise :two_factor_backupable, otp_number_of_backup_codes: 10 + serialize :otp_backup_codes, JSON + + devise :lockable, :async, :recoverable, :rememberable, :trackable, + :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -137,7 +150,7 @@ class User < ActiveRecord::Base validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } - validate :avatar_type, if: ->(user) { user.avatar_changed? } + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } @@ -236,6 +249,18 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end + + def reference_prefix + '@' + end + + # Pattern used to extract `@user` user references from text + def reference_pattern + %r{ + #{Regexp.escape(reference_prefix)} + (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR}) + }x + end end # @@ -246,6 +271,10 @@ class User < ActiveRecord::Base username end + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{username}" + end + def notification @notification ||= Notification.new(self) end @@ -298,7 +327,7 @@ class User < ActiveRecord::Base if primary_email_record primary_email_record.destroy self.emails.create(email: self.email_was) - + self.update_secondary_emails! end end @@ -438,7 +467,7 @@ class User < ActiveRecord::Base end def project_deploy_keys - DeployKey.in_projects(self.authorized_projects.pluck(:id)) + DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) end def accessible_deploy_keys @@ -454,7 +483,7 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w(name username skype linkedin twitter bio).each do |attr| + %w(name username skype linkedin twitter).each do |attr| value = self.send(attr) self.send("#{attr}=", Sanitize.clean(value)) if value.present? end @@ -626,6 +655,12 @@ class User < ActiveRecord::Base end end + def namespaces + namespace_ids = groups.pluck(:id) + namespace_ids.push(namespace.id) + Namespace.where(id: namespace_ids) + end + def oauth_authorized_tokens Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end @@ -660,4 +695,8 @@ class User < ActiveRecord::Base true end + + def can_be_removed? + !solo_owned_groups.present? + end end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb new file mode 100644 index 00000000000..9017a63af3b --- /dev/null +++ b/app/services/delete_user_service.rb @@ -0,0 +1,16 @@ +class DeleteUserService + def execute(user) + if user.solo_owned_groups.present? + user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' + user + else + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + user.destroy + end + end +end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb new file mode 100644 index 00000000000..d929a676293 --- /dev/null +++ b/app/services/destroy_group_service.rb @@ -0,0 +1,17 @@ +class DestroyGroupService + attr_accessor :group, :current_user + + def initialize(group, user) + @group, @current_user = group, user + end + + def execute + @group.projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + @group.destroy + end +end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index bd245100955..4d02752454e 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -13,5 +13,12 @@ module Files def repository project.repository end + + def after_commit(sha) + commit = repository.commit(sha) + full_ref = 'refs/heads/' + (params[:new_branch] || ref) + old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA + GitPushService.new.execute(project, current_user, old_sha, sha, full_ref) + end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 23833aa78ec..0a80455bc6b 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,7 +1,7 @@ require_relative "base_service" module Files - class CreateService < BaseService + class CreateService < Files::BaseService def execute allowed = Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) @@ -33,16 +33,24 @@ module Files end end + content = + if params[:encoding] == 'base64' + Base64.decode64(params[:content]) + else + params[:content] + end - new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path) - created_successfully = new_file_action.commit!( - params[:content], + sha = repository.commit_file( + current_user, + file_path, + content, params[:commit_message], - params[:encoding], - params[:new_branch] + params[:new_branch] || ref ) - if created_successfully + + if sha + after_commit(sha) success else error("Your changes could not be committed, because the file has been changed") diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 1497a0f883b..2281777604c 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,7 +1,7 @@ require_relative "base_service" module Files - class DeleteService < BaseService + class DeleteService < Files::BaseService def execute allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) @@ -19,14 +19,15 @@ module Files return error("You can only edit text files") end - delete_file_action = Gitlab::Satellite::DeleteFileAction.new(current_user, project, ref, path) - - deleted_successfully = delete_file_action.commit!( - nil, - params[:commit_message] + sha = repository.remove_file( + current_user, + path, + params[:commit_message], + ref ) - if deleted_successfully + if sha + after_commit(sha) success else error("Your changes could not be committed, because the file has been changed") diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 0724d3ae634..013cc1ee322 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,7 +1,7 @@ require_relative "base_service" module Files - class UpdateService < BaseService + class UpdateService < Files::BaseService def execute allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) @@ -19,14 +19,22 @@ module Files return error("You can only edit text files") end - edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path) - edit_file_action.commit!( - params[:content], + content = + if params[:encoding] == 'base64' + Base64.decode64(params[:content]) + else + params[:content] + end + + sha = repository.commit_file( + current_user, + path, + content, params[:commit_message], - params[:encoding], - params[:new_branch] + params[:new_branch] || ref ) + after_commit(sha) success rescue Gitlab::Satellite::CheckoutFailed => ex error("Your changes could not be committed because ref '#{ref}' could not be checked out", 400) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bdf36af02fd..cde65349d5c 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -127,7 +127,8 @@ class GitPushService end def is_default_branch?(ref) - Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch + Gitlab::Git.branch_ref?(ref) && + (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?) end def commit_user(commit) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 5e1906ad2ae..1d99223cfe6 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,17 +2,28 @@ class IssuableBaseService < BaseService private def create_assignee_note(issuable) - Note.create_assignee_change_note( + SystemNoteService.change_assignee( issuable, issuable.project, current_user, issuable.assignee) end def create_milestone_note(issuable) - Note.create_milestone_change_note( + SystemNoteService.change_milestone( issuable, issuable.project, current_user, issuable.milestone) end def create_labels_note(issuable, added_labels, removed_labels) - Note.create_labels_change_note( + SystemNoteService.change_label( issuable, issuable.project, current_user, added_labels, removed_labels) end + + def create_title_change_note(issuable, old_title) + SystemNoteService.change_title( + issuable, issuable.project, current_user, old_title) + end + + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) + SystemNoteService.change_branch( + issuable, issuable.project, current_user, branch_type, + old_branch, new_branch) + end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index f670019cc63..138465859ce 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -14,7 +14,7 @@ module Issues private def create_note(issue, current_commit) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit) end end end diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 1e5c398516d..e48ca359f4f 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -14,7 +14,7 @@ module Issues private def create_note(issue) - Note.create_status_change_note(issue, issue.project, current_user, issue.state, nil) + SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil) end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 8f04a69287a..6af942a5ca4 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -37,6 +37,10 @@ module Issues notification_service.reassigned_issue(issue, current_user) end + if issue.previous_changes.include?('title') + create_title_change_note(issue, issue.previous_changes['title'].first) + end + issue.notice_added_references(issue.project, current_user) execute_hooks(issue, 'update') end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index f6e1ae6f283..e455fe95791 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -2,7 +2,7 @@ module MergeRequests class BaseService < ::IssuableBaseService def create_note(merge_request) - Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) + SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end def hook_data(merge_request, action) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e9b526d1fb7..d0648da049b 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -10,6 +10,7 @@ module MergeRequests close_merge_requests reload_merge_requests + execute_mr_web_hooks comment_mr_with_commits true @@ -82,8 +83,23 @@ module MergeRequests mr_commit_ids.include?(commit.id) end - Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, new_commits, existing_commits, @oldrev) + SystemNoteService.add_commits(merge_request, merge_request.project, + @current_user, new_commits, + existing_commits, @oldrev) + end + end + + # Call merge request webhook with update branches + def execute_mr_web_hooks + merge_requests = @project.origin_merge_requests.opened + .where(source_branch: @branch_name) + .to_a + merge_requests += @fork_merge_requests.where(source_branch: @branch_name) + .to_a + merge_requests = filter_merge_requests(merge_requests) + + merge_requests.each do |merge_request| + execute_hooks(merge_request, 'update') end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 23af2656c37..4f6c6cba9a9 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -5,10 +5,11 @@ require_relative 'close_service' module MergeRequests class UpdateService < MergeRequests::BaseService def execute(merge_request) - # We dont allow change of source/target projects + # We don't allow change of source/target projects and source branch # after merge request was created params.except!(:source_project_id) params.except!(:target_project_id) + params.except!(:source_branch) state = params[:state_event] @@ -41,6 +42,12 @@ module MergeRequests ) end + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end + if merge_request.previous_changes.include?('milestone_id') create_milestone_note(merge_request) end @@ -50,6 +57,15 @@ module MergeRequests notification_service.reassigned_merge_request(merge_request, current_user) end + if merge_request.previous_changes.include?('title') + create_title_change_note(merge_request, merge_request.previous_changes['title'].first) + end + + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked + end + merge_request.notice_added_references(merge_request.project, current_user) execute_hooks(merge_request, 'update') end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index d19a6c2eca3..0ff37c41743 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -31,7 +31,7 @@ module Notes def execute_hooks(note) note_data = hook_data(note) - # TODO: Support Webhooks + note.project.execute_hooks(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks) end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 0d7ffbeebd9..312b56eb87b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -91,10 +91,14 @@ class NotificationService # * project team members with notification level higher then Participating # def merge_mr(merge_request, current_user) - recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project) + recipients = [merge_request.author, merge_request.assignee] + + recipients = add_project_watchers(recipients, merge_request.target_project) + recipients = reject_muted_users(recipients, merge_request.target_project) + recipients = add_subscribed_users(recipients, merge_request) recipients = reject_unsubscribed_users(recipients, merge_request) - recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq + recipients.delete(current_user) recipients.each do |recipient| @@ -137,20 +141,17 @@ class NotificationService recipients = recipients.concat(participants) # Merge project watchers - recipients = recipients.concat(project_watchers(note.project)).compact.uniq + recipients = add_project_watchers(recipients, note.project) # Reject users with Mention notification level, except those mentioned in _this_ note. recipients = reject_mention_users(recipients - note.mentioned_users, note.project) recipients = recipients + note.mentioned_users - # Reject mutes users recipients = reject_muted_users(recipients, note.project) recipients = add_subscribed_users(recipients, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - # Reject author recipients.delete(note.author) # build notify method like 'note_commit_email' @@ -287,6 +288,10 @@ class NotificationService users end + def add_project_watchers(recipients, project) + recipients.concat(project_watchers(project)).compact.uniq + end + # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) @@ -403,11 +408,13 @@ class NotificationService [target.author, target.assignee] end - recipients = reject_muted_users(recipients, project) + recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) + recipients = reject_muted_users(recipients, project) + recipients = add_subscribed_users(recipients, target) - recipients = recipients.concat(project_watchers(project)).uniq recipients = reject_unsubscribed_users(recipients, target) + recipients end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7e1d753b021..403f419ec50 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -1,28 +1,69 @@ module Projects class DestroyService < BaseService + include Gitlab::ShellAdapter + + class DestroyError < StandardError; end + + DELETED_FLAG = '+deleted' + def execute return false unless can?(current_user, :remove_project, project) project.team.truncate project.repository.expire_cache unless project.empty_repo? - if project.destroy - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace - ) + repo_path = project.path_with_namespace + wiki_path = repo_path + '.wiki' - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace + ".wiki" - ) + Project.transaction do + project.destroy! - project.satellite.destroy + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator') + end - log_info("Project \"#{project.name}\" was removed") - system_hook_service.execute_hooks_for(project, :destroy) - true + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator') + end end + + project.satellite.destroy + log_info("Project \"#{project.name}\" was removed") + system_hook_service.execute_hooks_for(project, :destroy) + true + end + + private + + def remove_repository(path) + # Skip repository removal. We use this flag when remove user or group + return true if params[:skip_repo] == true + + # There is a possibility project does not have repository or wiki + return true unless gitlab_shell.exists?(path + '.git') + + new_path = removal_path(path) + + if gitlab_shell.mv_repository(path, new_path) + log_info("Repository \"#{path}\" moved to \"#{new_path}\"") + GitlabShellWorker.perform_in(5.minutes, :remove_repository, new_path) + else + false + end + end + + def raise_error(message) + raise DestroyError.new(message) + end + + # Build a path for removing repositories + # We use `+` because its not allowed by GitLab so user can not create + # project with name cookies+119+deleted and capture someone stalled repository + # + # gitlab/cookies.git -> gitlab/cookies+119+deleted.git + # + def removal_path(path) + "#{path}+#{project.id}#{DELETED_FLAG}" end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index c5d0b08845b..60235b6be2a 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -7,12 +7,12 @@ class SystemHooksService def execute_hooks(data) SystemHook.all.each do |sh| - async_execute_hook sh, data + async_execute_hook(sh, data, 'system_hooks') end end - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data) + def async_execute_hook(hook, data, hook_name) + Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name) end def build_event_data(model, event) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb new file mode 100644 index 00000000000..b6801a92330 --- /dev/null +++ b/app/services/system_note_service.rb @@ -0,0 +1,317 @@ +# SystemNoteService +# +# Used for creating system notes (e.g., when a user references a merge request +# from an issue, an issue's assignee changes, an issue is closed, etc.) +class SystemNoteService + # Called when commits are added to a Merge Request + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # new_commits - Array of Commits added since last push + # existing_commits - Array of Commits added in a previous push + # oldrev - Optional String SHA of a previous Commit + # + # See new_commit_summary and existing_commit_summary. + # + # Returns the created Note object + def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil) + total_count = new_commits.length + existing_commits.length + commits_text = "#{total_count} commit".pluralize(total_count) + + body = "Added #{commits_text}:\n\n" + body << existing_commit_summary(noteable, existing_commits, oldrev) + body << new_commit_summary(new_commits).join("\n") + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the assignee of a Noteable is changed or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # assignee - User being assigned, or nil + # + # Example Note text: + # + # "Assignee removed" + # + # "Reassigned to @rspeicher" + # + # Returns the created Note object + def self.change_assignee(noteable, project, author, assignee) + body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when one or more labels on a Noteable are added and/or removed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # added_labels - Array of Labels added + # removed_labels - Array of Labels removed + # + # Example Note text: + # + # "Added ~1 and removed ~2 ~3 labels" + # + # "Added ~4 label" + # + # "Removed ~5 label" + # + # Returns the created Note object + def self.change_label(noteable, project, author, added_labels, removed_labels) + labels_count = added_labels.count + removed_labels.count + + references = ->(label) { "~#{label.id}" } + added_labels = added_labels.map(&references).join(' ') + removed_labels = removed_labels.map(&references).join(' ') + + body = '' + + if added_labels.present? + body << "added #{added_labels}" + body << ' and ' if removed_labels.present? + end + + if removed_labels.present? + body << "removed #{removed_labels}" + end + + body << ' ' << 'label'.pluralize(labels_count) + body = "#{body.capitalize}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the milestone of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # milestone - Milestone being assigned, or nil + # + # Example Note text: + # + # "Milestone removed" + # + # "Miletone changed to 7.11" + # + # Returns the created Note object + def self.change_milestone(noteable, project, author, milestone) + body = 'Milestone ' + body += milestone.nil? ? 'removed' : "changed to #{milestone.title}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the status of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # status - String status + # source - Mentionable performing the change, or nil + # + # Example Note text: + # + # "Status changed to merged" + # + # "Status changed to closed by bc17db76" + # + # Returns the created Note object + def self.change_status(noteable, project, author, status, source) + body = "Status changed to #{status}" + body += " by #{source.gfm_reference}" if source + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the title of a Noteable is changed + # + # noteable - Noteable object that responds to `title` + # project - Project owning noteable + # author - User performing the change + # old_title - Previous String title + # + # Example Note text: + # + # "Title changed from **Old** to **New**" + # + # Returns the created Note object + def self.change_title(noteable, project, author, old_title) + return unless noteable.respond_to?(:title) + + body = "Title changed from **#{old_title}** to **#{noteable.title}**" + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a branch in Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # branch_type - 'source' or 'target' + # old_branch - old branch name + # new_branch - new branch nmae + # + # Example Note text: + # + # "Target branch changed from `Old` to `New`" + # + # Returns the created Note object + def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when a Mentionable references a Noteable + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # author - User performing the reference + # + # Example Note text: + # + # "mentioned in #1" + # + # "mentioned in !2" + # + # "mentioned in 54f7727c" + # + # See cross_reference_note_content. + # + # Returns the created Note object + def self.cross_reference(noteable, mentioner, author) + return if cross_reference_disallowed?(noteable, mentioner) + + gfm_reference = mentioner.gfm_reference(noteable.project) + + note_options = { + project: noteable.project, + author: author, + note: cross_reference_note_content(gfm_reference) + } + + if noteable.kind_of?(Commit) + note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) + else + note_options.merge!(noteable: noteable) + end + + create_note(note_options) + end + + def self.cross_reference?(note_text) + note_text.start_with?(cross_reference_note_prefix) + end + + # Check if a cross-reference is disallowed + # + # This method prevents adding a "mentioned in !1" note on every single commit + # in a merge request. + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_disallowed?(noteable, mentioner) + return false unless mentioner.is_a?(MergeRequest) + return false unless noteable.is_a?(Commit) + + mentioner.commits.include?(noteable) + end + + # Check if a cross reference to a noteable from a mentioner already exists + # + # This method is used to prevent multiple notes being created for a mention + # when a issue is updated, for example. + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean + def self.cross_reference_exists?(noteable, mentioner) + # Initial scope should be system notes of this noteable type + notes = Note.system.where(noteable_type: noteable.class) + + if noteable.is_a?(Commit) + # Commits have non-integer IDs, so they're stored in `commit_id` + notes = notes.where(commit_id: noteable.id) + else + notes = notes.where(noteable_id: noteable.id) + end + + gfm_reference = mentioner.gfm_reference(noteable.project) + notes = notes.where(note: cross_reference_note_content(gfm_reference)) + + notes.count > 0 + end + + private + + def self.create_note(args = {}) + Note.create(args.merge(system: true)) + end + + def self.cross_reference_note_prefix + 'mentioned in ' + end + + def self.cross_reference_note_content(gfm_reference) + "#{cross_reference_note_prefix}#{gfm_reference}" + end + + # Build an Array of lines detailing each commit added in a merge request + # + # new_commits - Array of new Commit objects + # + # Returns an Array of Strings + def self.new_commit_summary(new_commits) + new_commits.collect do |commit| + "* #{commit.short_id} - #{commit.title}" + end + end + + # Build a single line summarizing existing commits being added in a merge + # request + # + # noteable - MergeRequest object + # existing_commits - Array of existing Commit objects + # oldrev - Optional String SHA of a previous Commit + # + # Examples: + # + # "* ea0f8418...2f4426b7 - 24 commits from branch `master`" + # + # "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`" + # + # "* ea0f8418 - 1 commit from branch `feature`" + # + # Returns a newline-terminated String + def self.existing_commit_summary(noteable, existing_commits, oldrev = nil) + return '' if existing_commits.empty? + + count = existing_commits.size + + commit_ids = if count == 1 + existing_commits.first.short_id + else + if oldrev + "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" + else + "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" + end + end + + commits_text = "#{count} commit".pluralize(count) + + branch = noteable.target_branch + branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork? + + "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" + end +end diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index 21ec2c01cb8..e85e58751e7 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,6 +1,6 @@ class TestHookService def execute(hook, current_user) data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user) - hook.execute(data) + hook.execute(data, 'push_hooks') end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index b9d7e8b4586..188a08940ab 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -30,8 +30,14 @@ .checkbox = f.label :twitter_sharing_enabled do = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - %strong Twitter enabled + Twitter enabled %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled %fieldset %legend Misc .form-group @@ -64,6 +70,11 @@ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' %span.help-block#home_help_block We will redirect non-logged in users to this page .form-group + = f.label :after_sign_out_path, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group = f.label :sign_in_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 @@ -77,6 +88,13 @@ .col-sm-10 = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form_group + = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :user_oauth_applications do + = f.check_box :user_oauth_applications + Allow users to register any application to use GitLab as an OAuth provider .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d1c586328a2..3732ff847b9 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -56,7 +56,12 @@ %span.light.pull-right = boolean_to_icon Gitlab.config.omniauth.enabled .col-md-4 - %h4 Components + %h4 + Components + - if current_application_settings.version_check_enabled + .pull-right + = version_status_badge + %hr %p GitLab diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 367d25cd6a1..6405a69fad3 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -19,8 +19,7 @@ = link_to admin_deploy_key_path(deploy_key) do %strong= deploy_key.title %td - %span - (#{deploy_key.fingerprint}) + %code.key-fingerprint= deploy_key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/admin/deploy_keys/show.html.haml b/app/views/admin/deploy_keys/show.html.haml deleted file mode 100644 index ea361ca4bdb..00000000000 --- a/app/views/admin/deploy_keys/show.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -- page_title @deploy_key.title, "Deploy Keys" -.row - .col-md-4 - .panel.panel-default - .panel-heading - Deploy Key - %ul.well-list - %li - %span.light Title: - %strong= @deploy_key.title - %li - %span.light Created on: - %strong= @deploy_key.created_at.stamp("Aug 21, 2011") - - .panel.panel-default - .panel-heading Projects (#{@deploy_key.deploy_keys_projects.count}) - - if @deploy_key.deploy_keys_projects.any? - %ul.well-list - - @deploy_key.projects.each do |project| - %li - %span - %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - .pull-right - = link_to disable_namespace_project_deploy_key_path(project.namespace, project, @deploy_key), data: { confirm: "Are you sure?" }, method: :put, class: "btn-xs btn btn-remove", title: 'Remove deploy key from project' do - %i.fa.fa-times.fa-inverse - - .col-md-8 - %p - %span.light Fingerprint: - %strong= @deploy_key.fingerprint - %pre.well-pre - = @deploy_key.key - .pull-right - = link_to 'Remove', admin_deploy_key_path(@deploy_key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index fe648470233..45dee86b017 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -79,11 +79,12 @@ %i.fa.fa-envelope = mail_to user.email, user.email, class: 'light' - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-sm" + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - unless user == current_user - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-sm success" + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-remove" - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 7fc85206109..f7195ac3326 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -140,18 +140,22 @@ .panel-heading Remove user .panel-body - %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + - if @user.can_be_removed? + %p Deleting a user has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + %br + = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + - else - if @user.solo_owned_groups.present? - %li - Next groups with all content will be removed: + %p + This user is currently an owner in these groups: %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + %p + You must transfer ownership or delete these groups before you can delete this user. #profile.tab-pane .row diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 5ecd53cff84..cfb386e131f 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -23,10 +23,9 @@ %i.fa.fa-cogs Settings - - if can?(current_user, :destroy_group_member, group_member) - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do - %i.fa.fa-sign-out - Leave + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do + %i.fa.fa-sign-out + Leave = image_tag group_icon(group), class: "avatar s40 avatar-tile" = link_to group, class: 'group-name' do diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 6e88fc9be40..07bda1c77f8 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{current_user.name} issues" - xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index dfdf0d68c8f..0dd2edbb1bc 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -17,5 +17,5 @@ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do %i.fa.fa-rss - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :issues = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index a7e1b08a0a4..61d2fbe538c 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -7,5 +7,5 @@ List all merge requests from all projects you have access to. %hr .append-bottom-20 - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :merge_requests = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 21e730bb7ff..d6f3e029a38 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -3,10 +3,10 @@ = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) .row .col-sm-6 - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = link_to issues_dashboard_path(milestone_title: milestone.title) do = pluralize milestone.issue_count, 'Issue' - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request' %span.light #{milestone.percent_complete}% complete diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 24f0bcb60d5..0d204ced7ea 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -56,6 +56,9 @@ Participants %span.badge= @dashboard_milestone.participants.count + .pull-right + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" + .tab-content .tab-pane.active#tab-issues .row diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder index 71edb73cd8a..e9a612231d5 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Activity" - xml.link href: dashboard_url(format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_url, rel: "alternate", type: "text/html" xml.id dashboard_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index e8820daf58f..29ffe8a8be3 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -6,7 +6,7 @@ .devise-errors = devise_error_messages! .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true + = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email] .clearfix = f.submit "Reset password", class: "btn-primary btn" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml new file mode 100644 index 00000000000..22b2c1a186b --- /dev/null +++ b/app/views/devise/sessions/two_factor.html.haml @@ -0,0 +1,10 @@ +%div + .login-box + .login-heading + %h3 Two-factor Authentication + .login-body + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true + %p.help-block.hint If you've lost your phone, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 8dce0b16936..f8ba9d80ae8 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,6 +5,6 @@ - providers.each do |provider| %span.light - if default_providers.include?(provider) - = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), class: 'oauth-image-link' + = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), method: :post, class: 'oauth-image-link' - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn", "data-no-turbolink" => "true" + = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), method: :post, class: "btn", "data-no-turbolink" => "true" diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index c86ce9ae651..742b74a67c7 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '' - = gfm event_commit_title(commit[:message]), project + = gfm event_commit_title(commit[:message]), project: project diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 0edb61ea246..4259f64c191 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if issue.description.present? - = markdown(issue.description, xhtml: true) + = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 1a8b62abeab..e8ed13df783 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if merge_request.description.present? - = markdown(merge_request.description, xhtml: true) + = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index b49c331ccf2..cfbfba50202 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(note.note, xhtml: true) + = markdown(note.note, xhtml: true, reference_only_path: false, project: note.project) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 5d14def8f75..3625cb49d8b 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), xhtml: true) + %blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project) - if event.commits_count > 15 %p %i diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 4ef18c09060..07bec1697f5 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -14,7 +14,7 @@ .event-note .md %i.fa.fa-comment-o.event-note-icon - = event_note(event.target.note) + = event_note(event.target.note, project: event.project) - note = event.target - if note.attachment.url - if note.attachment.image? diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 60d7978b13f..34a7c00dc43 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -4,8 +4,8 @@ - if event.rm_ref? %strong= event.ref_name - else - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name + %strong + = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) at = link_to_project event.project @@ -17,15 +17,27 @@ - few_commits.each do |commit| = render "events/commit", commit: commit, project: project + - create_mr = current_user == event.author && event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - if event.commits_count > 1 %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. + - if event.md_ref? - from = event.commit_from - from_label = truncate_sha(from) - else - from = event.project.default_branch - from_label = from + = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do - %strong Compare → #{from_label}...#{truncate_sha(event.commit_to)} + Compare #{from_label}...#{truncate_sha(event.commit_to)} + + - if create_mr + or + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + create a merge request + - elsif create_mr + %li.commits-stat + = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do + Create Merge Request diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 56b1948a474..ec39a755f0f 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -40,7 +40,8 @@ - if current_user == user = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + = icon("sign-out") + Leave - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 6a3da6adacf..e0756e909be 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -21,5 +21,5 @@ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do %i.fa.fa-rss - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :issues = render 'shared/issues' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 268f33d5761..3d9e857cc52 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -10,5 +10,5 @@ To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. %hr .append-bottom-20 - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :merge_requests = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 30093d2d05d..ba30e6e07c6 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -9,10 +9,10 @@ = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) .row .col-sm-6 - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = link_to issues_group_path(@group, milestone_title: milestone.title) do = pluralize milestone.issue_count, 'Issue' - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request' %span.light #{milestone.percent_complete}% complete diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 6c41cd6b9e4..8f2decb851f 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -62,6 +62,9 @@ Participants %span.badge= @group_milestone.participants.count + .pull-right + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped" + .tab-content .tab-pane.active#tab-issues .row diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index b52e78faaa3..a91d1a6e94b 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@group.name} activity" - xml.link href: group_url(@group, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1678311141e..0687840af39 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,7 +11,7 @@ @#{@group.path} - if @group.description.present? .description - = escaped_autolink(@group.description) + = markdown(@group.description, pipeline: :description) %hr = render 'shared/show_aside' diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index af39dfeac5b..bf4b7234b21 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -3,6 +3,8 @@ GitLab %span= Gitlab::VERSION %small= Gitlab::REVISION + - if current_application_settings.version_check_enabled + = version_status_badge %p.slead GitLab is open source software to collaborate on code. %br diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml deleted file mode 100644 index ef685a0434e..00000000000 --- a/app/views/layouts/_head_panel.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - .container - %div.app_logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do - = brand_header_logo - %h3 GitLab - %h1.title - = title - - %button.navbar-toggle{type: 'button', data: {target: '.navbar-collapse', toggle: 'collapse'}} - %span.sr-only Toggle navigation - = icon('bars') - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.hidden-sm.hidden-xs - = render 'layouts/search' - %li.visible-sm.visible-xs - = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('search') - %li - = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('question-circle') - %li - = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('globe') - %li - = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('clipboard') - - if current_user.is_admin? - %li - = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('cogs') - - if current_user.can_create_project? - %li - = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('plus') - %li - = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('user') - %li - = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('sign-out') - %li.hidden-xs - = link_to current_user, class: 'profile-pic', id: 'profile-pic', data: {toggle: 'tooltip', placement: 'bottom'} do - = image_tag avatar_icon(current_user.email, 60), alt: 'User activity' - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 5c55bdb5465..c1283734d25 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -7,6 +7,15 @@ = render 'layouts/nav/dashboard' .collapse-nav = render partial: 'layouts/collapse_button' + - if current_user + .sidebar-user + = link_to current_user, class: 'profile-pic', id: 'profile-pic', data: {toggle: 'tooltip', placement: 'top'} do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32' + .username + = current_user.username + .logout-holder + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'top'} do + = icon('sign-out') .content-wrapper .container-fluid .content diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml deleted file mode 100644 index 8a297566d6c..00000000000 --- a/app/views/layouts/_public_head_panel.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - .container - %div.app_logo - = link_to explore_root_path, class: "home" do - = brand_header_logo - %h3 GitLab - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - - unless current_controller?('sessions') - .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10' - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.visible-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 04f79846858..e2d2dec7ab8 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input" + = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control" = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index a97feeb1ecd..155825cc4c2 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -3,8 +3,8 @@ = render "layouts/head" %body{class: "#{app_theme}", :'data-page' => body_data_page} - if current_user - = render "layouts/head_panel", title: header_title + = render "layouts/header/default", title: header_title - else - = render "layouts/public_head_panel", title: header_title + = render "layouts/header/public", title: header_title = render 'layouts/page', sidebar: sidebar diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 5a59c9fd59a..d406f5764a7 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,7 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_mars.login-page.application - = render "layouts/empty_head_panel" + = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container .content diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index aa0f3f0a819..2e3a2b16eb7 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,7 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{app_theme} application"} - = render "layouts/head_panel", title: "" if current_user + = render "layouts/header/empty" .container.navless-container = render "layouts/flash" .error-page diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml new file mode 100644 index 00000000000..2970af377f5 --- /dev/null +++ b/app/views/layouts/header/_default.html.haml @@ -0,0 +1,43 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + %h3 GitLab + .header-content + %h1.title + = title + + %button.navbar-toggle + %span.sr-only Toggle navigation + = icon('bars') + + .navbar-collapse.collapse + %ul.nav.navbar-nav.pull-right + %li.hidden-sm.hidden-xs + = render 'layouts/search' + %li.visible-sm.visible-xs + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('search') + %li + = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('question-circle fw') + %li + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('globe fw') + %li + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('clipboard fw') + - if current_user.is_admin? + %li + = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('wrench fw') + - if current_user.can_create_project? + %li + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('plus fw') + %li + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('cog fw') + += render 'shared/outdated_browser' diff --git a/app/views/layouts/_empty_head_panel.html.haml b/app/views/layouts/header/_empty.html.haml index 358caa3868b..16fbf6d4020 100644 --- a/app/views/layouts/_empty_head_panel.html.haml +++ b/app/views/layouts/header/_empty.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab +%header.navbar.navbar-fixed-top.navbar-empty .container %h4.center = image_tag 'logo-white.png', width: 32, height: 32 diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml new file mode 100644 index 00000000000..6a031722aaa --- /dev/null +++ b/app/views/layouts/header/_public.html.haml @@ -0,0 +1,14 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to explore_root_path, class: "home" do + = brand_header_logo + %h3 GitLab + .header-content + %h1.title= title + + - unless current_controller?('sessions') + .pull-right + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success btn-sm' + += render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 62f0579d48b..9f1654b25b4 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -44,7 +44,7 @@ = link_to edit_group_path(@group), title: 'Group', data: {placement: 'right'} do = icon('pencil-square-o') %span - Group + Group Settings = nav_link(path: 'groups#projects') do = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do = icon('folder') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 31d8ed3ed86..ac37fd4c1c1 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -4,7 +4,7 @@ = icon('user fw') %span Profile - = nav_link(controller: :accounts) do + = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do = icon('gear fw') %span diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 21260302a09..7dd14449def 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -12,7 +12,7 @@ = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do = icon('pencil-square-o') %span - Project + Project Settings = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do = icon('users') diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 00c7cedce40..ee1b57278b6 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -27,7 +27,7 @@ } .file-stats .deleted-file { color: #B00; - }} + } %body %div.content = yield diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 9799b4cc4d7..3193206fe12 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,5 @@ -- page_title "Profile" -- header_title "Profile", profile_path +- page_title "Settings" +- header_title "Settings", profile_path - sidebar "profile" = render template: "layouts/application" diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index 0cc62935498..fc64c98038b 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,5 +1,5 @@ New Issue was created. Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> -Author: <%= @issue.author_name %> -Asignee: <%= @issue.assignee_name %> +Author: <%= @issue.author_name %> +Assignee: <%= @issue.assignee_name %> diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index f08039ad045..bdcca6e4ab7 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -3,6 +3,6 @@ New Merge Request #<%= @merge_request.iid %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> -Author: <%= @merge_request.author_name %> -Asignee: <%= @merge_request.assignee_name %> +Author: <%= @merge_request.author_name %> +Assignee: <%= @merge_request.assignee_name %> diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index ebbe98dd472..4feacdaacff 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -11,4 +11,6 @@ - if @user.created_by_id %p - = link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) + = link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token) + %p + = reset_token_expire_message diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb index 96b26879a77..dd9b71e3b84 100644 --- a/app/views/notify/new_user_email.text.erb +++ b/app/views/notify/new_user_email.text.erb @@ -5,4 +5,6 @@ The Administrator created an account for you. Now you are a member of the compan login.................. <%= @user.email %> <% if @user.created_by_id %> <%= link_to "Click here to set your password", edit_password_url(@user, :reset_password_token => @token) %> + + <%= reset_token_expire_message %> <% end %> diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index a374a662333..12f83aae04b 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -35,7 +35,7 @@ = diff.new_path - elsif diff.new_file %span.new-file - + + + = diff.new_path - else = diff.new_path diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 1c3a3d68aca..a26d4e0c757 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,13 +1,18 @@ - page_title "Account" +%h3.page-title + = page_title +%p.light + Change your username and basic account settings. +%hr - if current_user.ldap_user? .alert.alert-info Some options are unavailable for LDAP accounts .account-page - %fieldset.update-token - %legend + .panel.panel-default.update-token + .panel-heading Reset Private token - %div + .panel-body = form_for @user, url: reset_private_token_profile_path, method: :put do |f| .data %p @@ -21,58 +26,91 @@ - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" %div - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn success btn-build-token" + = f.submit 'Generate', class: "btn btn-default btn-build-token" + - unless current_user.ldap_user? + .panel.panel-default + .panel-heading + Two-factor Authentication + .panel-body + - if current_user.otp_required_for_login + .pull-right + = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', + data: { confirm: 'Are you sure?' } + %p.text-success + %strong + Two-factor Authentication is enabled + %p + If you lose your recovery codes you can + %strong + = succeed ',' do + = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } + invalidating all previous codes. + + - else + %p + Increase your account's security by enabling two-factor authentication (2FA). + %p + Each time you log in you’ll be required to provide your username and + password as usual, plus a randomly-generated code from your phone. + %div + = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - if show_profile_social_tab? - %fieldset - %legend Connected Accounts - .oauth-buttons.append-bottom-10 - %p Click on icon to activate signin with one of the following services - - enabled_social_providers.each do |provider| - .btn-group - = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), - class: "btn btn-lg #{'active' if oauth_active?(provider)}" - - if oauth_active?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do - %i.fa.fa-close + .panel.panel-default + .panel-heading + Connected Accounts + .panel-body + .oauth-buttons.append-bottom-10 + %p Click on icon to activate signin with one of the following services + - enabled_social_providers.each do |provider| + .btn-group + = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), + method: :post, class: "btn btn-lg #{'active' if oauth_active?(provider)}" + - if oauth_active?(provider) + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do + = icon('close') - if show_profile_username_tab? - %fieldset.update-username - %legend + .panel.panel-warning.update-username + .panel-heading Change Username - = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| - %p - Changing your username will change path to all personal projects! - %div - = f.text_field :username, required: true, class: 'form-control' - - .loading-gif.hide + .panel-body + = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| %p - %i.fa.fa-spinner.fa-spin - Saving new username - %p.light - = user_url(@user) - %div - = f.submit 'Save username', class: "btn btn-warning" + Changing your username will change path to all personal projects! + %div + = f.text_field :username, required: true, class: 'form-control' + + .loading-gif.hide + %p + = icon('spinner spin') + Saving new username + %p.light + = user_url(@user) + %div + = f.submit 'Save username', class: "btn btn-warning" - if show_profile_remove_tab? - %fieldset.remove-account - %legend + .panel.panel-danger.remove-account + .panel-heading Remove account - %div - %p Deleting an account has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = current_user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if current_user.solo_owned_groups.present? - %li - The following groups will be abandoned. You should transfer or remove them: - %strong #{current_user.solo_owned_groups.map(&:name).join(', ')} - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - + .panel-body + - if @user.can_be_removed? + %p Deleting an account has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = current_user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + - if @user.solo_owned_groups.present? + %p + Your account is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete yur account. diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index c4f6f59624b..2c4f0804f0b 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -1,34 +1,44 @@ - page_title "Applications" %h3.page-title - Application Settings + = page_title %p.light - OAuth2 protocol settings below. + - if user_oauth_applications? + Manage applications that can use GitLab as an OAuth provider, + and applications that you've authorized to use your account. + - else + Manage applications that you've authorized to use your account. +%hr -%fieldset.oauth-applications - %legend Your applications - %p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' - - if @applications.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th Clients - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td - - application.redirect_uri.split.each do |uri| - %div= uri - %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' - %td= render 'doorkeeper/applications/delete_form', application: application +- if user_oauth_applications? + .oauth-applications + %h3 + Your applications + .pull-right + = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' + - if @applications.any? + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application -%fieldset.oauth-authorized-applications.prepend-top-20 - %legend Authorized applications +.oauth-authorized-applications.prepend-top-20 + - if user_oauth_applications? + %h3 + Authorized applications - if @authorized_tokens.any? %table.table.table-striped diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml index af284f60409..f450ec1c018 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/design.html.haml @@ -1,54 +1,56 @@ - page_title "Design" %h3.page-title - Design Settings + = page_title %p.light Appearance settings will be saved to your profile and made available across all devices. %hr = form_for @user, url: profile_path, remote: true, method: :put do |f| - %fieldset.application-theme - %legend + .panel.panel-default.application-theme + .panel-heading Application theme - .themes_opts - = label_tag do - .prev.default - = f.radio_button :theme_id, 1 - Graphite + .panel-body + .themes_opts + = label_tag do + .prev.default + = f.radio_button :theme_id, 1 + Graphite - = label_tag do - .prev.classic - = f.radio_button :theme_id, 2 - Charcoal + = label_tag do + .prev.classic + = f.radio_button :theme_id, 2 + Charcoal - = label_tag do - .prev.modern - = f.radio_button :theme_id, 3 - Green + = label_tag do + .prev.modern + = f.radio_button :theme_id, 3 + Green - = label_tag do - .prev.gray - = f.radio_button :theme_id, 4 - Gray + = label_tag do + .prev.gray + = f.radio_button :theme_id, 4 + Gray - = label_tag do - .prev.violet - = f.radio_button :theme_id, 5 - Violet + = label_tag do + .prev.violet + = f.radio_button :theme_id, 5 + Violet - = label_tag do - .prev.blue - = f.radio_button :theme_id, 6 - Blue - %br - .clearfix + = label_tag do + .prev.blue + = f.radio_button :theme_id, 6 + Blue + %br + .clearfix - %fieldset.code-preview-theme - %legend + .panel.panel-default.code-preview-theme + .panel-heading Code preview theme - .code_highlight_opts - - color_schemes.each do |color_scheme_id, color_scheme| - = label_tag do - .prev - = image_tag "#{color_scheme}-scheme-preview.png" - = f.radio_button :color_scheme_id, color_scheme_id - = color_scheme.gsub(/[-_]+/, ' ').humanize + .panel-body + .code_highlight_opts + - color_schemes.each do |color_scheme_id, color_scheme| + = label_tag do + .prev + = image_tag "#{color_scheme}-scheme-preview.png" + = f.radio_button :color_scheme_id, color_scheme_id + = color_scheme.gsub(/[-_]+/, ' ').humanize diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 2c0d0e10a4c..66812872c41 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,23 +1,27 @@ - page_title "Emails" %h3.page-title - Email Settings + = page_title %p.light - Your - %b Primary Email - will be used for avatar detection and web based operations, such as edits and merges. -%p.light - Your - %b Notification Email - will be used for account notifications. -%p.light - Your - %b Public Email - will be displayed on your public profile. -%p.light - All email addresses will be used to identify your commits. - + Control emails linked to your account %hr + +%ul + %li + Your + %b Primary Email + will be used for avatar detection and web based operations, such as edits and merges. + %li + Your + %b Notification Email + will be used for account notifications. + %li + Your + %b Public Email + will be displayed on your public profile. + %li + All email addresses will be used to identify your commits. + .panel.panel-default .panel-heading Emails (#{@emails.count + 1}) diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index fe5770f45c3..9bbccbc45ea 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -3,8 +3,7 @@ = link_to path_to_key(key, is_admin) do %strong= key.title %td - %span - (#{key.fingerprint}) + %code.key-fingerprint= key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 8bac22a2e1a..e0ae4d9720f 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -15,7 +15,7 @@ .col-md-8 %p %span.light Fingerprint: - %strong= @key.fingerprint + %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key .pull-right diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index e3af0d4e189..06655f7ba3a 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,6 +1,6 @@ - page_title "SSH Keys" %h3.page-title - SSH Keys Settings + = page_title .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" %p.light diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index a74d97dac3b..9480a19f5b2 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,6 +1,6 @@ - page_title "Notifications" %h3.page-title - Notifications Settings + = page_title %p.light These are your global notification settings. %hr diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 21dabbdfe2c..399ae98adf9 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,5 +1,6 @@ - page_title "Password" -%h3.page-title Password Settings +%h3.page-title + = page_title %p.light - if @user.password_automatically_set? Set your password. diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 29c30905117..6534afb0e89 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,6 +1,6 @@ -- page_title "Settings" +- page_title "Profile" %h3.page-title - Profile Settings + = page_title %p.light This information will appear on your profile. - if current_user.ldap_user? @@ -37,8 +37,11 @@ = f.text_field :email, class: "form-control", required: true - if @user.unconfirmed_email.present? %span.help-block - Please click the link in the confirmation email before continuing, it was sent to - %strong #{@user.unconfirmed_email} + Please click the link in the confirmation email before continuing. It was sent to + = succeed "." do + %strong #{@user.unconfirmed_email} + %p + = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post - else %span.help-block We also use email for avatar detection if no avatar is uploaded. diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml new file mode 100644 index 00000000000..1b1395eaa17 --- /dev/null +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -0,0 +1,11 @@ +%p.slead + Should you ever lose your phone, each of these recovery codes can be used one + time each to regain access to your account. Please save them in a safe place. + +.codes.well + %ul + - @codes.each do |code| + %li + %span.monospace= code + += link_to 'Proceed', profile_account_path, class: 'btn btn-success' diff --git a/app/views/profiles/two_factor_auths/codes.html.haml b/app/views/profiles/two_factor_auths/codes.html.haml new file mode 100644 index 00000000000..addf356697a --- /dev/null +++ b/app/views/profiles/two_factor_auths/codes.html.haml @@ -0,0 +1,5 @@ +- page_title 'Recovery Codes', 'Two-factor Authentication' + +%h3.page-title Two-factor Authentication Recovery codes +%hr += render 'codes' diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml new file mode 100644 index 00000000000..e330aadac13 --- /dev/null +++ b/app/views/profiles/two_factor_auths/create.html.haml @@ -0,0 +1,6 @@ +- page_title 'Two-factor Authentication', 'Account' + +.alert.alert-success + Congratulations! You have enabled Two-factor Authentication! + += render 'codes' diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml new file mode 100644 index 00000000000..b9f3e2380fe --- /dev/null +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -0,0 +1,39 @@ +- page_title 'Two-factor Authentication', 'Account' + +%h2.page-title Two-Factor Authentication (2FA) +%p + Download the Google Authenticator application from App Store for iOS or + Google Play for Android and scan this code. + +%hr + += form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f| + - if @error + .alert.alert-danger + = @error + .form-group + .col-sm-2 + .col-sm-2 + = raw @qr_code + .col-sm-8.manual-instructions + %h3 Can't scan the code? + + %p + To add the entry manually, provide the following details to the + application on your phone. + + %dl + %dt Account + %dd= current_user.email + %dl + %dt Key + %dd= current_user.otp_secret.scan(/.{4}/).join(' ') + %dl + %dt Time based + %dd Yes + .form-group + = label_tag :pin_code, nil, class: "control-label" + .col-sm-10 + = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true + .form-actions + = submit_tag 'Submit', class: 'btn btn-success' diff --git a/app/views/projects/_aside.html.haml b/app/views/projects/_aside.html.haml index 1865b5be8c6..c9c17110d2b 100644 --- a/app/views/projects/_aside.html.haml +++ b/app/views/projects/_aside.html.haml @@ -1,83 +1,108 @@ .clearfix - .append-bottom-20 - = render "shared/clone_panel" - - unless @project.empty_repo? - .well - %h4 Repository - %ul.nav.nav-pills - %li= link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) - %li= link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project) - %li= link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project) + .panel.panel-default + .panel-heading + = visibility_level_icon(@project.visibility_level) + = "#{visibility_level_label(@project.visibility_level).capitalize} project" + + .panel-body + - if @repository.changelog || @repository.license || @repository.contribution_guide + %ul.nav.nav-pills + - if @repository.changelog + %li.hidden-xs + = link_to changelog_url(@project) do + Changelog + - if @repository.license + %li + = link_to license_url(@project) do + License + - if @repository.contribution_guide + %li + = link_to contribution_guide_url(@project) do + Contribution guide + + .actions + - if can? current_user, :write_issue, @project + = link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm append-right-10' do + = icon("exclamation-circle fw") + New Issue - .actions - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm' do - %i.fa.fa-exchange - Compare code + - if can? current_user, :write_merge_request, @project + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-sm", title: "New Merge Request" do + = icon("plus fw") + New Merge Request + + - if forked_from_project = @project.forked_from_project + .panel-footer + = icon("code-fork fw") + Forked from + .pull-right + = link_to forked_from_project.namespace.try(:name), project_path(forked_from_project) + + + - @project.ci_services.each do |ci_service| + - if ci_service.active? && ci_service.respond_to?(:builds_path) + .panel-footer + = icon("check fw") + = ci_service.title + .pull-right + - if ci_service.respond_to?(:status_img_path) + = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do + = image_tag ci_service.status_img_path, alt: "build status", class: 'ci-status-image' + - else + = link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - - if can?(current_user, :download_code, @project) - - = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm' - unless @project.empty_repo? - .well - %h4 Contribute - %ul.nav.nav-pills - - if @repository.changelog - %li.hidden-xs - = link_to changelog_url(@project) do - Changelog - - if @repository.contribution_guide - %li.hidden-xs - = link_to contribution_guide_url(@project) do - Contribution guide - - if @repository.license + .panel.panel-default + .panel-heading + = icon("folder-o fw") + Repository + .panel-body + %ul.nav.nav-pills + %li + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = pluralize(number_with_delimiter(@repository.commit_count), 'commit') %li - = link_to license_url(@project) do - License - .actions - = link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm' do - %i.fa.fa-fw.fa-exclamation-circle - New issue - - if can? current_user, :write_merge_request, @project - - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-sm", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request + = link_to namespace_project_branches_path(@project.namespace, @project) do + = pluralize(number_with_delimiter(@repository.branch_names.count), 'branch') + %li + = link_to namespace_project_tags_path(@project.namespace, @project) do + = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') + + .actions + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm append-right-10' do + %i.fa.fa-exchange + Compare code + - if can?(current_user, :download_code, @project) + = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm' + - if version = @repository.version + .panel-footer + = icon("clock-o fw") + Version + .pull-right + = link_to version_url(@project) do + = @repository.blob_by_oid(version.id).data + = render "shared/clone_panel" - if @project.archived? + %br .alert.alert-warning %h4 - %i.fa.fa-exclamation-triangle + = icon("exclamation-triangle fw") Archived project! %p Repository is read-only - - if @project.forked_from_project - .well - %h4 - Forked from - .pull-right - = link_to @project.forked_from_project.namespace.try(:name), project_path(@project.forked_from_project) - - -- if version = @repository.version - .well - %h4 - Version - .pull-right - = link_to version_url(@project) do - = @repository.blob_by_oid(version.id).data - -- @project.ci_services.each do |ci_service| - - if ci_service.active? && ci_service.respond_to?(:builds_path) - .well - %h4 - = ci_service.title - .pull-right - - if ci_service.respond_to?(:status_img_path) - = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do - = image_tag ci_service.status_img_path, alt: "build status" - - else - = link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' + - if current_user + - access = user_max_access_in_project(current_user, @project) + - if access + .light-well.light.prepend-top-20 + %small + You have #{access} access to this project. + - if @project.project_member_by_id(current_user) + %br + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave this project diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml deleted file mode 100644 index d623a3716ed..00000000000 --- a/app/views/projects/_dropdown.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- if current_user - .dropdown.pull-right - %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-bars - %ul.dropdown-menu - - if @project.issues_enabled && can?(current_user, :write_issue, @project) - %li - = link_to url_for_new_issue(@project, only_path: true), title: "New Issue" do - %i.fa.fa-fw.fa-exclamation-circle - New issue - - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) - %li - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), title: "New Merge Request" do - %i.fa.fa-fw.fa-tasks - New merge request - - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) - %li - = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do - %i.fa.fa-fw.fa-file-text-o - New snippet - - if can?(current_user, :admin_project_member, @project) - %li - = link_to namespace_project_project_members_path(@project.namespace, @project), title: "New project member" do - %i.fa.fa-fw.fa-users - New project member - - if can? current_user, :push_code, @project - %li.divider - %li - = link_to new_namespace_project_branch_path(@project.namespace, @project) do - %i.fa.fa-fw.fa-code-fork - New branch - %li - = link_to new_namespace_project_tag_path(@project.namespace, @project) do - %i.fa.fa-fw.fa-tag - New tag - - diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index f9cdda4a3ba..076afb11a9d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? - = escaped_autolink(@project.description) + = markdown(@project.description, pipeline: :description) - if can?(current_user, :admin_project, @project) – = link_to 'Edit', edit_namespace_project_path diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index e321a84974e..2c5b24b8130 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -11,6 +11,15 @@ .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, class: 'form-control pad js-gfm-input', required: true + + - if issuable.is_a?(MergeRequest) + %p.help-block + - if issuable.work_in_progress? + Remove the <code>WIP</code> prefix from the title to allow this + <strong>Work In Progress</strong> merge request to be accepted when it's ready. + - else + Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a + <strong>Work In Progress</strong> merge request from being accepted before it's ready. .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 @@ -37,7 +46,7 @@ .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", placeholder: 'Select a user', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id) + selected: issuable.assignee_id, project: @target_project || @project) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group @@ -70,6 +79,24 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank +- if issuable.is_a?(MergeRequest) + %hr + - unless @merge_request.persisted? + .form-group + = f.label :source_branch, class: 'control-label' do + %i.fa.fa-code-fork + Source Branch + .col-sm-10 + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + %p.help-block + = link_to 'Change source branch', mr_change_branches_path(@merge_request) + .form-group + = f.label :target_branch, class: 'control-label' do + %i.fa.fa-code-fork + Target Branch + .col-sm-10 + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2' }) + .form-actions - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? %p diff --git a/app/views/projects/_section.html.haml b/app/views/projects/_section.html.haml index 0b7f4cb780a..f4f876f3809 100644 --- a/app/views/projects/_section.html.haml +++ b/app/views/projects/_section.html.haml @@ -1,10 +1,12 @@ %ul.nav.nav-tabs %li.active = link_to '#tab-activity', 'data-toggle' => 'tab' do + = icon("tachometer") Activity - if @repository.readme %li = link_to '#tab-readme', 'data-toggle' => 'tab' do + = icon("file-text-o") Readme .tab-content .tab-pane.active#tab-activity diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 462f5b7afb0..8019c7f4569 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -32,5 +32,5 @@ %code :erb <% lines.each do |line| %> - <%= highlight(@blob.name, line, true).html_safe %> + <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> <% end %> diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 96f188e4aa7..9c3e1703c89 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -12,8 +12,8 @@ \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name' - .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + .pull-right + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' .file-content.code %pre.js-edit-mode-pane#editor diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index f6bd62f239b..4429c395aee 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,8 +1,4 @@ -- if gitlab_markdown?(blob.name) - .file-content.wiki - = preserve do - = markdown(blob.data) -- elsif markup?(blob.name) +- if markup?(blob.name) .file-content.wiki = render_markup(blob.name, blob.data) - else diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 5c79d0ef11f..84742608986 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -2,7 +2,7 @@ - if @form.unfold? && @form.since != 1 && !@form.bottom? %tr.line_holder{ id: @form.since } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false} + line_old: @form.since, line_new: @form.since, bottom: false, new_file: false} - @lines.each_with_index do |line, index| - line_new = index + @form.since @@ -16,4 +16,4 @@ - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } = render "projects/diffs/match_line", {line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true} + line_old: @form.to, line_new: @form.to, bottom: true, new_file: false} diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4e7415be4aa..43412624da6 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -10,16 +10,19 @@ %i.fa.fa-lock protected .pull-right - - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-xs' + - if create_mr_button?(@repository.root_ref, branch.name) + = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do + = icon('plus') + Merge Request + - if branch.name != @repository.root_ref = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do - %i.fa.fa-files-o + = icon("exchange") Compare - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + = icon("trash-o") - if commit %ul.list-unstyled diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index a714f5f79e0..e3d8cd0fdd5 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,17 +1,22 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do + = icon("history") Commits - %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',') + %span.badge= number_with_delimiter(@repository.commit_count) = nav_link(controller: :compare) do - = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do + = icon("exchange") + Compare = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do + = icon("code-fork") Branches %span.badge.js-totalbranch-count= @repository.branches.size = nav_link(controller: :tags) do = link_to namespace_project_tags_path(@project.namespace, @project) do + = icon("tags") Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 01edd9447ce..3854ad5d611 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name}:#{@ref} commits" - xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any? diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index c8531b090a6..9682100a54c 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -8,11 +8,17 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' -- if current_user && current_user.private_token - .commits-feed-holder.hidden-xs.hidden-sm - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do - %i.fa.fa-rss - Commits feed +.commits-feed-holder.hidden-xs.hidden-sm + - if create_mr_button?(@repository.root_ref, @ref) + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request + + - if current_user && current_user.private_token + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'prepend-left-10 btn' do + = icon("rss") + Commits Feed + %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index dfb1dded9ea..a0e904cfd8b 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -13,9 +13,10 @@ = text_field_tag :to, params[:to], class: "form-control" = button_tag "Compare", class: "btn btn-create commits-compare-btn" - - if compare_to_mr_button? - = link_to compare_mr_path, class: 'prepend-left-10 btn' do - %strong Make a merge request + - if create_mr_button? + = link_to create_mr_path, class: 'prepend-left-10 btn' do + = icon("plus") + Create Merge Request :javascript diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index c577dfa8d55..8d66bae8cdf 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -2,24 +2,20 @@ .pull-right - if @available_keys.include?(deploy_key) = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-plus + = icon('plus') Enable - else - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" - else = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-power-off + = icon('power-off') Disable - - if project = project_for_deploy_key(deploy_key) - = link_to namespace_project_deploy_key_path(project.namespace, project, deploy_key) do - %i.fa.fa-key - %strong= deploy_key.title - - else - %i.fa.fa-key - %strong= deploy_key.title - + = icon('key') + %strong= deploy_key.title + %br + %code.key-fingerprint= deploy_key.fingerprint %p.light.prepend-top-10 - if deploy_key.public? diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml deleted file mode 100644 index 7d44652af72..00000000000 --- a/app/views/projects/deploy_keys/show.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- page_title @key.title, "Deploy Keys" -%h3.page-title - Deploy key: - = @key.title - %small - created on - = @key.created_at.stamp("Aug 21, 2011") -.back-link - = link_to namespace_project_deploy_keys_path(@project.namespace, @project) do - ← To keys list -%hr -%pre= @key.key -.pull-right - = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index d4b019780f5..99ee23a1ddc 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,8 +10,9 @@ - if @commit.parent_ids.present? = view_file_btn(@commit.parent_id, diff_file, project) - elsif diff_file.diff.submodule? - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id, project.repository) + %span + - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) + = submodule_link(submodule_item, @commit.id, project.repository) - else %span - if diff_file.renamed_file diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml index 4ebe3379733..d1f897b99f7 100644 --- a/app/views/projects/diffs/_match_line.html.haml +++ b/app/views/projects/diffs/_match_line.html.haml @@ -1,7 +1,7 @@ -%td.old_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_old}, - class: unfold_bottom_class(bottom)} +%td.old_line.diff-line-num{data: {linenumber: line_old}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... -%td.new_line.diff-line-num.unfold.js-unfold{data: {linenumber: line_new}, - class: unfold_bottom_class(bottom)} +%td.new_line.diff-line-num{data: {linenumber: line_new}, + class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} \... %td.line_content.matched= line diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e6dfbfd6511..a6373181b45 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -12,7 +12,7 @@ %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" = render "projects/diffs/match_line", {line: line.text, - line_old: line_old, line_new: line.new_pos, bottom: false} + line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file} - else %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code @@ -29,7 +29,7 @@ - if last_line > 0 = render "projects/diffs/match_line", {line: "", - line_old: last_line, line_new: last_line, bottom: true} + line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file} - if diff_file.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 808c03148f4..eadbf61fdd4 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -35,6 +35,13 @@ %p.light This url will be triggered when a new tag is pushed to the repository %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + %div = f.check_box :issues_events, class: 'pull-left' .prepend-left-20 = f.label :issues_events, class: 'list-label' do @@ -64,6 +71,6 @@ .clearfix %span.monospace= hook.url %p - - %w(push_events tag_push_events issues_events merge_requests_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 2016f5c709c..48858fa32da 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -30,5 +30,4 @@ %label Labels .issue-show-labels - @issue.labels.each do |label| - = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do - = render_colored_label(label) + = link_to_label(label) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index ef36d1f9547..a4e25e5ce88 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -8,8 +8,7 @@ = link_to_gfm issue.title, issue_path(issue), class: "row_title" .issue-labels - issue.labels.each do |label| - = link_to namespace_project_issues_path(issue.project.namespace, issue.project, label_name: label.name) do - = render_colored_label(label) + = link_to_label(label, project: issue.project) .pull-right.light - if issue.closed? %span diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 5fa8fbdf893..dc8e477185b 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} issues" - xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 709ea1f7897..1d5597602d1 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -14,11 +14,11 @@ = render 'shared/issuable_search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :write_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do %i.fa.fa-plus New Issue - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :issues .issues-holder = render "issues" diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 82829452862..7fa1ee53f76 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,8 +1,8 @@ %li{id: dom_id(label)} - = render_colored_label(label) + = link_to_label(label) .pull-right %strong.append-right-20 - = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do + = link_to_label(label) do = pluralize label.open_issues_count, 'open issue' - if can? current_user, :admin_label, @project diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 7d19415a7f4..d44fe486212 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -13,4 +13,7 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - if can? current_user, :admin_label, @project + .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - else + .nothing-here-block No labels created diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 9a2aa9c3de0..eb3dba6858d 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -27,5 +27,4 @@ %label Labels .merge-request-show-labels - @merge_request.labels.each do |label| - = link_to namespace_project_merge_requests_path(@project.namespace, @project, label_name: label.name) do - = render_colored_label(label) + = link_to_label(label) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5d5a23b5409..65f5c3d6a19 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -4,17 +4,16 @@ = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" .merge-request-labels - merge_request.labels.each do |label| - = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, label_name: label.name) do - = render_colored_label(label) + = link_to_label(label, project: merge_request.project) .pull-right.light - if merge_request.merged? %span %i.fa.fa-check - MERGED + ACCEPTED - elsif merge_request.closed? %span - %i.fa.fa-close - CLOSED + %i.fa.fa-ban + REJECTED - else %span.hidden-xs.hidden-sm %span.label-branch< diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 24a9563dd4d..9a2edbf0a8c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -19,30 +19,31 @@ .mr-compare.merge-request %ul.nav.nav-tabs.merge-request-tabs - %li.commits-tab{data: {action: 'commits', toggle: 'tab'}} - = link_to url_for(params) do - %i.fa.fa-history + %li.commits-tab + = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') Commits %span.badge= @commits.size - %li.diffs-tab{data: {action: 'diffs', toggle: 'tab'}} - = link_to url_for(params) do - %i.fa.fa-list-alt + %li.diffs-tab + = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') Changes %span.badge= @diffs.size - .commits.tab-content - = render "projects/commits/commits", project: @project - .diffs.tab-content - - if @diffs.present? - = render "projects/diffs/diffs", diffs: @diffs, project: @project - - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .alert.alert-danger - %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. - %p To preserve performance the line changes are not shown. - - else - .alert.alert-danger - %h4 This comparison includes a huge diff. - %p To preserve performance the line changes are not shown. + .tab-content + #commits.commits.tab-pane + = render "projects/commits/commits", project: @project + #diffs.diffs.tab-pane + - if @diffs.present? + = render "projects/diffs/diffs", diffs: @diffs, project: @project + - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + .alert.alert-danger + %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. + %p To preserve performance the line changes are not shown. + - else + .alert.alert-danger + %h4 This comparison includes a huge diff. + %p To preserve performance the line changes are not shown. :javascript $('.assign-to-me-link').on('click', function(e){ @@ -55,6 +56,8 @@ :javascript var merge_request merge_request = new MergeRequest({ - action: 'commits' + action: 'new', + diffs_loaded: true, + commits_loaded: true }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index c2f5cdacae7..74f8b9950cf 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -22,44 +22,44 @@ %span into %strong.label-branch #{@merge_request.target_branch} - if @merge_request.open? - %span.pull-right - .btn-group - %a.btn.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .btn-group.btn-group-sm.pull-right + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/show/state_widget" - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs - %li.notes-tab{data: {action: 'notes', toggle: 'tab'}} - = link_to merge_request_path(@merge_request) do - %i.fa.fa-comments + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do + = icon('comments') Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab{data: {action: 'commits', toggle: 'tab'}} - = link_to merge_request_path(@merge_request), title: 'Commits' do - %i.fa.fa-history + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = icon('history') Commits %span.badge= @commits.size - %li.diffs-tab{data: {action: 'diffs', toggle: 'tab'}} - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do - %i.fa.fa-list-alt + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = icon('list-alt') Changes %span.badge= @merge_request.diffs.size - .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } - = render "projects/merge_requests/discussion" - .commits.tab-content - = render "projects/merge_requests/show/commits" - .diffs.tab-content - - if current_page?(action: 'diffs') - = render "projects/merge_requests/show/diffs" + .tab-content + #notes.notes.tab-pane.voting_notes + = render "projects/merge_requests/discussion" + #commits.commits.tab-pane + = render "projects/merge_requests/show/commits" + #diffs.diffs.tab-pane + - if current_page?(action: 'diffs') + = render "projects/merge_requests/show/diffs" .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index ab845a7e719..841d1e1cfe9 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -7,6 +7,6 @@ = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do %i.fa.fa-plus New Merge Request - = render 'shared/issuable_filter' + = render 'shared/issuable_filter', type: :merge_requests .merge-requests-holder = render 'merge_requests' diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index a5a821c1847..1d0e2e350b0 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :modify_merge_request, @merge_request) - = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, null_user: true) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, project: @target_project, null_user: true) %div.prepend-top-20.clearfix .issuable-context-title diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index cb536214c69..906cc11dc67 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -1,14 +1,17 @@ - unless @allowed_to_merge - if @project.archived? %p - %strong Archived projects cannot be committed to! + %strong Archived projects do not provide commit access. - else .automerge_widget.cannot_be_merged.hide - %strong This request can't be merged automatically. Even if it could be merged, you don't have permission to do so. + %strong This merge request contains merge conflicts that must be resolved. + Only those with write access to this repository can merge merge requests. .automerge_widget.work_in_progress.hide - %strong This request can't be merged automatically because it is marked a Work In Progress. Even if it could be merged, you don't have permission to do so. + %strong This merge request is marked as Work In Progress. + Only those with write access to this repository can merge merge requests. .automerge_widget.can_be_merged.hide - %strong This request can be merged automatically, but you don't have permission to do so. + %strong This request can be merged automatically. + Only those with write access to this repository can merge merge requests. - if @show_merge_controls @@ -34,7 +37,7 @@ %br .light - If you still want to merge this request manually - use + If you want to merge this request manually, you can use the %strong = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" @@ -42,47 +45,45 @@ .automerge_widget.no_satellite.hide %p %span - %strong This repository does not have satellite. Ask an administrator to fix this issue + %strong This repository does not have a satellite. Please ask an administrator to fix this issue! .automerge_widget.cannot_be_merged.hide %h4 - This request can't be merged with GitLab. - You should do it manually with + This pull request contains merge conflicts that must be resolved. + You can try it manually on the %strong - = link_to "#modal_merge_info", class: "underlined-link how_to_merge_link", title: "How To Merge", "data-toggle" => "modal" do - command line + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" %p %button.btn.disabled{:type => 'button'} %i.fa.fa-warning Accept Merge Request - This usually happens when git can not resolve conflicts between branches automatically. + This happens when Git is not able to automatically resolve conflicts between branches. .automerge_widget.work_in_progress.hide %h4 - This request can't be merged because it is marked a <strong>Work In Progress</strong>. + This request cannot be merged because it is marked as <strong>Work In Progress</strong>. %p %button.btn.disabled{:type => 'button'} %i.fa.fa-warning Accept Merge Request - - When the merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. + When the merge request is ready, remove the "WIP" prefix from the title to allow merging. .automerge_widget.unchecked %p %strong %i.fa.fa-spinner.fa-spin - Checking for ability to automatically merge… + Checking automatic merge… .automerge_widget.already_cannot_be_merged.hide %p - %strong This merge request can not be merged. Try to reload the page. + %strong This merge request cannot be merged. Try to reload the page. .merge-in-progress.hide %p %i.fa.fa-spinner.fa-spin - Merge is in progress. Please wait. Page will be automatically reloaded. + Merge is in progress. Please wait… Page will be reloaded automatically. diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index ffa3f7b0e36..3b1cd53df37 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -1,34 +1,34 @@ - if @commits.any? .ci_widget.ci-success{style: "display:none"} - %i.fa.fa-check + = icon("check") %span CI build passed for #{@merge_request.last_commit_short_sha}. = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-failed{style: "display:none"} - %i.fa.fa-times + = icon("times") %span CI build failed for #{@merge_request.last_commit_short_sha}. = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - [:running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} - %i.fa.fa-clock-o + = icon("clock-o") %span CI build #{status} for #{@merge_request.last_commit_short_sha}. = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget - %i.fa.fa-spinner + = icon("spinner spin") Checking for CI status for #{@merge_request.last_commit_short_sha} .ci_widget.ci-canceled{style: "display:none"} - %i.fa.fa-times + = icon("times") %span CI build canceled for #{@merge_request.last_commit_short_sha}. = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-error{style: "display:none"} - %i.fa.fa-times + = icon("times") %span Cannot connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 46e92a9c558..0690fdb769f 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,9 +1,9 @@ %h4.page-title .issue-box{ class: issue_box_class(@merge_request) } - if @merge_request.merged? - Merged + Accepted - elsif @merge_request.closed? - Closed + Rejected - else Open = "Merge Request ##{@merge_request.iid}" diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index 44bd9347f51..6396232db22 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -11,17 +11,17 @@ - if @merge_request.closed? %h4 - Closed + Rejected - if @merge_request.closed_event - by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} + by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} %p Changes were not merged into target branch - if @merge_request.merged? %h4 - Merged + Accepted - if @merge_request.merge_event - by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} + by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} = render "projects/merge_requests/show/remove_source_branch" diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 62360158ff9..14a0580f966 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -13,10 +13,10 @@ = milestone.expires_at .row .col-sm-6 - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do = pluralize milestone.issues.count, 'Issue' - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do = pluralize milestone.merge_requests.count, 'Merge Request' %span.light #{milestone.percent_complete}% complete diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index bba2b8764ac..417eaa1b09d 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -61,12 +61,13 @@ Participants %span.badge= @users.count - - if @project.issues_enabled - .pull-right + .pull-right + - if can?(current_user, :write_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do %i.fa.fa-plus New Issue - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues @@ -85,10 +86,10 @@ .col-md-3 = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing') .col-md-3 - = render('merge_requests', title: 'Declined (closed)', merge_requests: @merge_requests.declined, id: 'closed') + = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.rejected, id: 'closed') .col-md-3 .panel.panel-primary - .panel-heading Merged + .panel-heading Accepted %ul.well-list - @merge_requests.merged.each do |merge_request| = render 'merge_request', merge_request: merge_request diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 4d26b52df01..0a77f200f56 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -8,27 +8,32 @@ = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: '' .timeline-content .note-header - .note-actions - = link_to "##{dom_id(note)}", name: dom_id(note) do - = icon('link') - Link here - - - if note_editable?(note) + - if note_editable?(note) + .note-actions = link_to '#', title: 'Edit comment', class: 'js-note-edit' do = icon('pencil-square-o') - Edit - - = link_to namespace_project_note_path(@project.namespace, @project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'danger js-note-delete' do - = icon('trash-o', class: 'cred') - Remove + + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = icon('trash-o') + + - unless note.system + - member = note.project.team.find_member(note.author.id) + - if member + %span.note-role.label + = member.human_access + - if note.system = link_to user_path(note.author) do = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: '' - = link_to_member(@project, note.author, avatar: false) + + = link_to_member(note.project, note.author, avatar: false) + %span.author-username = '@' + note.author.username + %span.note-last-update - = note_timestamp(note) + = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do + = note_timestamp(note) - if note.superceded?(@notes) - if note.upvote? @@ -65,7 +70,7 @@ = link_to note.attachment.url, target: '_blank' do = icon('paperclip') = note.attachment_identifier - = link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note), + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do = icon('trash-o', class: 'cred') .clear diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 7c6f7243173..e7a3854701c 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -14,7 +14,9 @@ - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) + %span.discussion-last-update #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')} + .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 635e4d70941..860a997cff8 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -38,8 +38,9 @@ - if current_user == user - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do - %i.fa.fa-minus.fa-inverse + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do + = icon("sign-out") + Leave - else = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index bb713dcafa5..242684e5c7c 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} activity" - xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 72916cad182..04590f65b27 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -8,7 +8,7 @@ - if can? current_user, :download_code, @project .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true #tree-holder.tree-holder.clearfix = render "tree", tree: @tree diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 4f3f4cab8d5..7d9bd08385a 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -6,4 +6,4 @@ $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.project-edit-content .btn-save').enableButton(); + $('.project-edit-content .btn-save').enable(); diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 6834969de8b..b2c085f34b1 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -8,6 +8,8 @@ = label_tag :new_wiki_path do %span Page slug = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) + %p.hidden.text-danger{data: { error: "slug" }} + The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / %p.hint Please don't use spaces. .modal-footer diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 2efa616d664..adfdd1c7506 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -11,6 +11,6 @@ #{merge_request.project.name_with_namespace} .pull-right - if merge_request.merged? - %span.label.label-primary Merged + %span.label.label-primary Accepted - elsif merge_request.closed? - %span.label.label-danger Closed + %span.label.label-danger Rejected diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 8af393777f0..95099853918 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -13,16 +13,7 @@ .file-title %i.fa.fa-file %strong= snippet_blob[:snippet_object].file_name - - if gitlab_markdown?(snippet_blob[:snippet_object].file_name) - .file-content.wiki - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = preserve do - = markdown(snippet[:data]) - - else - .file-content.code - .nothing-here-block Empty file - - elsif markup?(snippet_blob[:snippet_object].file_name) + - if markup?(snippet_blob[:snippet_object].file_name) .file-content.wiki - snippet_blob[:snippet_chunks].each do |snippet| - unless snippet[:data].empty? diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 60bb76e898a..3f489a04e71 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -18,7 +18,7 @@ :"data-container" => "body"} = gitlab_config.protocol.upcase = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", readonly: true - - if project.kind_of?(Project) + - if project.kind_of?(Project) && project.empty_repo? .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } = visibility_level_icon(project.visibility_level) diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index fa8b4eae314..a5187fa4ea7 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -3,15 +3,28 @@ %ul.nav.nav-tabs %li{class: ("active" if params[:state] == 'opened')} = link_to page_filter_path(state: 'opened') do - %i.fa.fa-exclamation-circle + = icon('exclamation-circle') #{state_filters_text_for(:opened, @project)} - %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed') do - %i.fa.fa-check-circle - #{state_filters_text_for(:closed, @project)} + + - if defined?(type) && type == :merge_requests + %li{class: ("active" if params[:state] == 'merged')} + = link_to page_filter_path(state: 'merged') do + = icon('check-circle') + #{state_filters_text_for(:merged, @project)} + + %li{class: ("active" if params[:state] == 'rejected')} + = link_to page_filter_path(state: 'rejected') do + = icon('ban') + #{state_filters_text_for(:rejected, @project)} + - else + %li{class: ("active" if params[:state] == 'closed')} + = link_to page_filter_path(state: 'closed') do + = icon('check-circle') + #{state_filters_text_for(:closed, @project)} + %li{class: ("active" if params[:state] == 'all')} = link_to page_filter_path(state: 'all') do - %i.fa.fa-compass + = icon('compass') #{state_filters_text_for(:all, @project)} .issues-details-filters @@ -51,8 +64,6 @@ = button_tag "Update issues", class: "btn update_selected_issues btn-save" :coffeescript - new UsersSelect() - $('form.filter-form').on 'submit', (event) -> event.preventDefault() Turbolinks.visit @.action + '&' + $(@).serialize() diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index b07c4d20f12..02416125a72 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -1,7 +1,7 @@ - Gitlab::VisibilityLevel.values.each do |level| .radio - restricted = restricted_visibility_levels.include?(level) - = label model_method, level do + = form.label "#{model_method}_#{level}" do = form.radio_button model_method, level, checked: (selected_level == level), disabled: restricted = visibility_level_icon(level) .option-title diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 30458793fd1..d26a99bb14c 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,9 +1,5 @@ - unless @snippet.content.empty? - - if gitlab_markdown?(@snippet.file_name) - .file-content.wiki - = preserve do - = markdown(@snippet.data) - - elsif markup?(@snippet.file_name) + - if markup?(@snippet.file_name) .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) - else diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 6783587bda9..2feeeecc48b 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet - + .form-group .file-editor = f.label :file_name, "File", class: 'control-label' @@ -29,7 +29,7 @@ - else = f.submit 'Save', class: "btn-save btn" - - if @snippet.respond_to?(:project) + - if @snippet.project_id = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index 73085c046bd..fb878965288 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -3,8 +3,8 @@ class ProjectWebHookWorker sidekiq_options queue: :project_web_hook - def perform(hook_id, data) + def perform(hook_id, data, hook_name) data = data.with_indifferent_access - WebHook.find(hook_id).execute(data) + WebHook.find(hook_id).execute(data, hook_name) end end diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index 3ebc62b7e7a..a122c274763 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -3,7 +3,7 @@ class SystemHookWorker sidekiq_options queue: :system_hook - def perform(hook_id, data) - SystemHook.find(hook_id).execute data + def perform(hook_id, data, hook_name) + SystemHook.find(hook_id).execute(data, hook_name) end end diff --git a/bin/guard b/bin/guard deleted file mode 100755 index 0c1a532bd01..00000000000 --- a/bin/guard +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env ruby -# -# This file was generated by Bundler. -# -# The application 'guard' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'pathname' -ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require 'rubygems' -require 'bundler/setup' - -load Gem.bin_path('guard', 'guard') @@ -3,6 +3,5 @@ begin load File.expand_path("../spring", __FILE__) rescue LoadError end -require_relative '../config/boot' -require 'rake' -Rake.application.run +require 'bundler/setup' +load Gem.bin_path('rake', 'rake') diff --git a/bin/spring b/bin/spring index 253ec37c345..7b45d374fcd 100755 --- a/bin/spring +++ b/bin/spring @@ -1,17 +1,14 @@ #!/usr/bin/env ruby -# This file loads spring without using Bundler, in order to be fast -# It gets overwritten when you run the `spring binstub` command +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) require "rubygems" require "bundler" - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) - ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) - ENV["GEM_HOME"] = "" - Gem.paths = ENV - + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) + Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } gem "spring", match[1] require "spring/binstub" end diff --git a/config.ru b/config.ru index e90863a5c21..a2525c81361 100644 --- a/config.ru +++ b/config.ru @@ -2,11 +2,14 @@ if defined?(Unicorn) require 'unicorn' - # Unicorn self-process killer - require 'unicorn/worker_killer' - # Max memory size (RSS) per worker - use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + if ENV['RAILS_ENV'] == 'production' || ENV['RAILS_ENV'] == 'staging' + # Unicorn self-process killer + require 'unicorn/worker_killer' + + # Max memory size (RSS) per worker + use Unicorn::WorkerKiller::Oom, (200 * (1 << 20)), (250 * (1 << 20)) + end end require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index fa399533e52..7e899cc3b5b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,7 +31,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters.push(:password, :password_confirmation, :private_token) + config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index bd2081688d1..c7f22b9388b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -182,6 +182,10 @@ production: &base # Allow login via Twitter, Google, etc. using OmniAuth providers enabled: false + # Uncomment this to automatically sign in with a specific omniauth provider's without + # showing GitLab's sign-in page (default: show the GitLab sign-in page) + # auto_sign_in_with_provider: saml + # CAUTION! # This allows users to login without having a user account first (default: false). # User accounts will be created automatically when authentication was successful. @@ -210,6 +214,15 @@ production: &base # args: { scope: 'api' } } # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'saml', + # args: { + # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + # idp_sso_target_url: 'https://login.example.com/idp', + # issuer: 'https://gitlab.example.com', + # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + # } } + @@ -245,6 +258,10 @@ production: &base repos_path: /home/git/repositories/ hooks_path: /home/git/gitlab-shell/hooks/ + # File that contains the secret key for verifying access for gitlab-shell. + # Default is '.gitlab_shell_secret' relative to Rails.root (i.e. root of the GitLab app). + # secret_file: /home/git/gitlab/.gitlab_shell_secret + # Git over HTTP upload_pack: true receive_pack: true diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e5ac66a2323..c234bd69e9a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -87,6 +87,8 @@ end Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? + Settings.omniauth['providers'] ||= [] Settings['issues_tracker'] ||= {} @@ -148,6 +150,7 @@ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}? Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/' Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/hooks/' +Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? Settings.gitlab_shell['repos_path'] ||= Settings.gitlab['user_home'] + '/repositories/' diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/6_rack_profiler.rb index 38a5fa98dc2..5312fd8e89a 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/6_rack_profiler.rb @@ -3,7 +3,8 @@ if Rails.env.development? # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) + Rack::MiniProfiler.config.position = 'right' Rack::MiniProfiler.config.start_hidden = true - Rack::MiniProfiler.config.skip_paths << '/specs' + Rack::MiniProfiler.config.skip_paths << '/teaspoon' end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 8f6c5673103..6f1f267bf97 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -10,3 +10,10 @@ if Gitlab::LDAP::Config.enabled? alias_method server['provider_name'], :ldap end end + +OmniAuth.config.allowed_request_methods = [:post] +#In case of auto sign-in, the GET method is used (users don't get to click on a button) +OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? +OmniAuth.config.before_request_phase do |env| + OmniAuth::RequestForgeryProtection.new(env).call +end diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb new file mode 100644 index 00000000000..c668864089b --- /dev/null +++ b/config/initializers/attr_encrypted_no_db_connection.rb @@ -0,0 +1,20 @@ +module AttrEncrypted + module Adapters + module ActiveRecord + def attribute_instance_methods_as_symbols_with_no_db_connection + # Use with_connection so the connection doesn't stay pinned to the thread. + connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false + + if connected + # Call version from AttrEncrypted::Adapters::ActiveRecord + attribute_instance_methods_as_symbols_without_no_db_connection + else + # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord + AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call + end + end + + alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection + end + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 8f8c4169740..091548348b1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,6 +1,11 @@ # Use this hook to configure devise mailer, warden hooks and so forth. The first # four configuration values can also be set straight in your models. Devise.setup do |config| + config.warden do |manager| + manager.default_strategies(scope: :user).unshift :two_factor_authenticatable + manager.default_strategies(scope: :user).unshift :two_factor_backupable + end + # ==> Mailer Configuration # Configure the class responsible to send e-mails. config.mailer = "DeviseMailer" diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb index e7c9f0ba7c2..751fccead07 100644 --- a/config/initializers/gitlab_shell_secret_token.rb +++ b/config/initializers/gitlab_shell_secret_token.rb @@ -5,8 +5,7 @@ require 'securerandom' # Your secret key for verifying the gitlab_shell. -secret_file = Rails.root.join('.gitlab_shell_secret') -gitlab_shell_symlink = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +secret_file = Gitlab.config.gitlab_shell.secret_file unless File.exist? secret_file # Generate a new token of 16 random hexadecimal characters and store it in secret_file. @@ -14,6 +13,7 @@ unless File.exist? secret_file File.write(secret_file, token) end -if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(gitlab_shell_symlink) - FileUtils.symlink(secret_file, gitlab_shell_symlink) +link_path = File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret') +if File.exist?(Gitlab.config.gitlab_shell.path) && !File.exist?(link_path) + FileUtils.symlink(secret_file, link_path) end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index c5b6b75e7f6..a4032a21420 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -31,7 +31,7 @@ en: messages: # Common error messages invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' - invalid_redirect_uri: 'The redirect uri included is not valid.' + invalid_redirect_uri: 'The redirect URI included is not valid.' unauthorized_client: 'The client is not authorized to perform this request using this method.' access_denied: 'The resource owner or authorization server denied the request.' invalid_scope: 'The requested scope is invalid, unknown, or malformed.' @@ -63,11 +63,11 @@ en: flash: applications: create: - notice: 'Application created.' + notice: 'The application was created successfully.' destroy: - notice: 'Application deleted.' + notice: 'The application was deleted successfully.' update: - notice: 'Application updated.' + notice: 'The application was updated successfully.' authorized_applications: destroy: - notice: 'Application revoked.' + notice: 'The application was revoked access.' diff --git a/config/routes.rb b/config/routes.rb index 4b38dede7b4..f4a104664f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ require 'sidekiq/web' require 'api/api' Gitlab::Application.routes.draw do - mount JasmineRails::Engine => '/specs' if defined?(JasmineRails) use_doorkeeper do controllers applications: 'oauth/applications', authorized_applications: 'oauth/authorized_applications', @@ -166,7 +165,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, only: [:index, :show, :new, :create, :destroy] + resources :deploy_keys, only: [:index, :new, :create, :destroy] resources :hooks, only: [:index, :create, :destroy] do get :test @@ -226,6 +225,11 @@ Gitlab::Application.routes.draw do resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] + resource :two_factor_auth, only: [:new, :create, :destroy] do + member do + post :codes + end + end end end @@ -417,7 +421,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :show, :new, :create] do + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do put :enable put :disable @@ -445,6 +449,7 @@ Gitlab::Application.routes.draw do resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do member do get :diffs + get :commits post :automerge get :automerge_check get :ci_status diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index ae4c0550a4f..87839770924 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -23,7 +23,7 @@ Sidekiq::Testing.inline! do name: group_path.titleize, path: group_path ) - group.description = Faker::Lorem.sentence + group.description = FFaker::Lorem.sentence group.save group.add_owner(User.first) @@ -35,7 +35,7 @@ Sidekiq::Testing.inline! do import_url: url, namespace_id: group.id, name: project_path.titleize, - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, visibility_level: Gitlab::VisibilityLevel.values.sample } diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index 24952a1f661..378354efd5a 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -2,9 +2,9 @@ Gitlab::Seeder.quiet do (2..20).each do |i| begin User.create!( - username: Faker::Internet.user_name, - name: Faker::Name.name, - email: Faker::Internet.email, + username: FFaker::Internet.user_name, + name: FFaker::Name.name, + email: FFaker::Internet.email, confirmed_at: DateTime.now, password: '12345678' ) diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index 2296821e528..a43116829d9 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -3,7 +3,7 @@ Gitlab::Seeder.quiet do (1..5).each do |i| milestone_params = { title: "v#{i}.0", - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, } diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index e8b01b46d22..c636e96381c 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -2,8 +2,8 @@ Gitlab::Seeder.quiet do Project.all.each do |project| (1..10).each do |i| issue_params = { - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentence, + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, milestone: project.milestones.sample, assignee: project.team.users.sample diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index f9b2fd8b05f..0825776ffaa 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -10,8 +10,8 @@ Gitlab::Seeder.quiet do params = { source_branch: source_branch, target_branch: target_branch, - title: Faker::Lorem.sentence(6), - description: Faker::Lorem.sentences(3).join(" "), + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.sentences(3).join(" "), milestone: project.milestones.sample, assignee: project.team.users.sample } diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb index b3a6f39c7d5..3bd4b442ade 100644 --- a/db/fixtures/development/12_snippets.rb +++ b/db/fixtures/development/12_snippets.rb @@ -28,8 +28,8 @@ eos PersonalSnippet.seed(:id, [{ id: i, author_id: user.id, - title: Faker::Lorem.sentence(3), - file_name: Faker::Internet.domain_word + '.rb', + title: FFaker::Lorem.sentence(3), + file_name: FFaker::Internet.domain_word + '.rb', visibility_level: Gitlab::VisibilityLevel.values.sample, content: content, }]) diff --git a/db/fixtures/development/13_comments.rb b/db/fixtures/development/13_comments.rb index d37be53c7b9..566c0705638 100644 --- a/db/fixtures/development/13_comments.rb +++ b/db/fixtures/development/13_comments.rb @@ -6,7 +6,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'Issue', noteable_id: issue.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute @@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do note_params = { noteable_type: 'MergeRequest', noteable_id: mr.id, - note: Faker::Lorem.sentence, + note: FFaker::Lorem.sentence, } Notes::CreateService.new(project, user, note_params).execute diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb new file mode 100644 index 00000000000..e9d42c1e749 --- /dev/null +++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddVersionCheckToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :version_check_enabled, :boolean, default: true + end +end diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb new file mode 100644 index 00000000000..11b026ee8f3 --- /dev/null +++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb @@ -0,0 +1,8 @@ +class AddDeviseTwoFactorToUsers < ActiveRecord::Migration + def change + add_column :users, :encrypted_otp_secret, :string + add_column :users, :encrypted_otp_secret_iv, :string + add_column :users, :encrypted_otp_secret_salt, :string + add_column :users, :otp_required_for_login, :boolean + end +end diff --git a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb new file mode 100644 index 00000000000..913958db7c5 --- /dev/null +++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb @@ -0,0 +1,5 @@ +class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration + def change + add_column :users, :otp_backup_codes, :text + end +end diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb index 3452fd45c4f..5d3e856ddce 100644 --- a/db/migrate/20150406133311_add_invite_data_to_member.rb +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -1,5 +1,5 @@ class AddInviteDataToMember < ActiveRecord::Migration - def change + def up add_column :members, :created_by_id, :integer add_column :members, :invite_email, :string add_column :members, :invite_token, :string @@ -9,4 +9,15 @@ class AddInviteDataToMember < ActiveRecord::Migration add_index :members, :invite_token, unique: true end + + def down + remove_index :members, :invite_token + + change_column :members, :user_id, :integer, null: false + + remove_column :members, :invite_accepted_at + remove_column :members, :invite_token + remove_column :members, :invite_email + remove_column :members, :created_by_id + end end diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb index c275b49d228..46cf63593c9 100644 --- a/db/migrate/20150417122318_remove_import_data_from_project.rb +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -1,5 +1,9 @@ class RemoveImportDataFromProject < ActiveRecord::Migration - def change + def up remove_column :projects, :import_data end + + def down + add_column :projects, :import_data, :text + end end diff --git a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb index 9b0f13f3fa7..50a9b2439e0 100644 --- a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb +++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb @@ -1,7 +1,11 @@ class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration - def change + def up add_column :application_settings, :default_project_visibility, :integer visibility = Settings.gitlab.default_projects_features['visibility_level'] execute("update application_settings set default_project_visibility = #{visibility}") end + + def down + remove_column :application_settings, :default_project_visibility + end end diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb index 1a9152cb965..13e5038db9c 100644 --- a/db/migrate/20150425164647_remove_duplicate_tags.rb +++ b/db/migrate/20150425164647_remove_duplicate_tags.rb @@ -1,7 +1,8 @@ class RemoveDuplicateTags < ActiveRecord::Migration def up select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| - duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag["name"]}'").map{|tag| tag["id"]} + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} origin_tag_id = duplicate_ids.first duplicate_ids.delete origin_tag_id diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb index 51237354d9f..8f1b0cc8935 100644 --- a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb +++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb @@ -1,7 +1,11 @@ class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration - def change + def up add_column :application_settings, :default_snippet_visibility, :integer visibility = Settings.gitlab.default_projects_features['visibility_level'] execute("update application_settings set default_snippet_visibility = #{visibility}") end + + def down + remove_column :application_settings, :default_snippet_visibility + end end diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb index 6013605bb35..244637e1c4a 100644 --- a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb +++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb @@ -1,6 +1,9 @@ class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration - def change + def up execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\ SELECT id FROM namespaces WHERE type='Group')") end + + def down + end end diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb new file mode 100644 index 00000000000..b02605489be --- /dev/null +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -0,0 +1,16 @@ +# Convert legacy Markdown-emphasized notes to the current, non-emphasized format +# +# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_ +# +# becomes +# +# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666 +class ConvertLegacyReferenceNotes < ActiveRecord::Migration + def up + execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'} + end + + def down + # noop + end +end diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb new file mode 100644 index 00000000000..0097587b4f6 --- /dev/null +++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb @@ -0,0 +1,9 @@ +class AddNoteEventsToWebHooks < ActiveRecord::Migration + def up + add_column :web_hooks, :note_events, :boolean, default: false, null: false + end + + def down + remove_column :web_hooks, :note_events, :boolean + end +end diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb new file mode 100644 index 00000000000..6a78294f0b2 --- /dev/null +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :user_oauth_applications, :bool, default: true + end +end diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb new file mode 100644 index 00000000000..83e08101407 --- /dev/null +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_out_path, :string + end +end
\ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 04abf9bb9a6..aea0742cf3b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150502064022) do +ActiveRecord::Schema.define(version: 20150529150354) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,10 +28,13 @@ ActiveRecord::Schema.define(version: 20150502064022) do t.integer "default_branch_protection", default: 2 t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" + t.boolean "version_check_enabled", default: true t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" + t.boolean "user_oauth_applications", default: true + t.string "after_sign_out_path" end create_table "broadcast_messages", force: true do |t| @@ -493,6 +496,11 @@ ActiveRecord::Schema.define(version: 20150502064022) do t.string "bitbucket_access_token_secret" t.string "location" t.string "public_email", default: "", null: false + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" + t.boolean "otp_required_for_login" + t.text "otp_backup_codes" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -527,6 +535,7 @@ ActiveRecord::Schema.define(version: 20150502064022) do t.boolean "issues_events", default: false, null: false t.boolean "merge_requests_events", default: false, null: false t.boolean "tag_push_events", default: false + t.boolean "note_events", default: false, null: false end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/doc/api/README.md b/doc/api/README.md index f6757b0a6aa..ca58c184543 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -19,6 +19,7 @@ - [Deploy Keys](deploy_keys.md) - [System Hooks](system_hooks.md) - [Groups](groups.md) +- [Namespaces](namespaces.md) ## Clients diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index c1d82ad9576..7b0873a9111 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -221,7 +221,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -231,7 +231,6 @@ Parameters: - `id` (required) - The ID of a project - `merge_request_id` (required) - ID of MR -- `source_branch` - The source branch - `target_branch` - The target branch - `assignee_id` - Assignee user ID - `title` - Title of MR @@ -242,7 +241,6 @@ Parameters: { "id": 1, "target_branch": "master", - "source_branch": "test1", "project_id": 3, "title": "test1", "description": "description1", diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md new file mode 100644 index 00000000000..7b3238441f6 --- /dev/null +++ b/doc/api/namespaces.md @@ -0,0 +1,44 @@ +# Namespaces + +## List namespaces + +Get a list of namespaces. (As user: my namespaces, as admin: all namespaces) + +``` +GET /namespaces +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + }, + { + "id": 2, + "path": "group1", + "kind": "group" + } +] +``` + +You can search for namespaces by name or path, see below. + +## Search for namespace + +Get all namespaces that match your string in their name or path. + +``` +GET /namespaces?search=foobar +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + } +] +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 971fe96fb8e..17c014019ea 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -32,6 +32,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ```json [ @@ -134,6 +135,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### List ALL projects @@ -149,6 +151,7 @@ Parameters: - `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first ### Get single project diff --git a/doc/development/README.md b/doc/development/README.md index d5d264be19d..6bc8e1888db 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,4 +1,4 @@ -# Development +# Development - [Architecture](architecture.md) of GitLab - [Shell commands](shell_commands.md) in the GitLab codebase @@ -6,3 +6,5 @@ - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements +- [Migration Style Guide](migration_style_guide.md) for creating safe migrations +- [How to dump production data to staging](dump_db.md) diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md new file mode 100644 index 00000000000..4ad3bd534e0 --- /dev/null +++ b/doc/development/db_dump.md @@ -0,0 +1,45 @@ +# Importing a database dump into a staging enviroment + +Sometimes it is useful to import the database from a production environment +into a staging environment for testing. The procedure below assumes you have +SSH+sudo access to both the production environment and the staging VM. + +On the staging VM, add the following line to `/etc/gitlab/gitlab.rb` to speed up +large database imports. + +``` +# On STAGING +echo "postgresql['checkpoint_segments'] = 64" | sudo tee -a /etc/gitlab/gitlab.rb +sudo touch /etc/gitlab/skip-auto-migrations +sudo gitlab-ctl reconfigure +``` + +Next, we let the production environment stream a compressed SQL dump to our +local machine via SSH, and redirect this stream to a psql client on the staging +VM. + +``` +# On LOCAL MACHINE +ssh -C gitlab.example.com sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -Cc gitlabhq_production |\ + ssh -C staging-vm sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -d template1 +``` + +## Recreating directory structure + +If you need to re-create some directory structure on the staging server you can +use this procedure. + +First, on the production server, create a list of directories you want to +re-create. + +``` +# On PRODUCTION +(umask 077; sudo find /var/opt/gitlab/git-data/repositories -maxdepth 1 -type d -print0 > directories.txt) +``` + +Copy `directories.txt` to the staging server and create the directories there. + +``` +# On STAGING +sudo -u git xargs -0 mkdir -p < directories.txt +``` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md new file mode 100644 index 00000000000..4fa1961fde9 --- /dev/null +++ b/doc/development/migration_style_guide.md @@ -0,0 +1,88 @@ +# Migration Style Guide + +When writing migrations for GitLab, you have to take into account that +these will be ran by hundreds of thousands of organizations of all sizes, some with +many years of data in their database. + +In addition, having to take a server offline for a an upgrade small or big is +a big burden for most organizations. For this reason it is important that your +migrations are written carefully, can be applied online and adhere to the style guide below. + +When writing your migrations, also consider that databases might have stale data +or inconsistencies and guard for that. Try to make as little assumptions as possible +about the state of the database. + +Please don't depend on GitLab specific code since it can change in future versions. +If needed copy-paste GitLab code into the migration to make make it forward compatible. + +## Comments in the migration + +Each migration you write needs to have the two following pieces of information +as comments. + +### Online, Offline, errors? + +First, you need to provide information on whether the migration can be applied: + +1. online without errors (works on previous version and new one) +2. online with errors on old instances after migrating +3. online with errors on new instances while migrating +4. offline (needs to happen without app servers to prevent db corruption) + +It is always preferable to have a migration run online. If you expect the migration +to take particularly long (for instance, if it loops through all notes), +this is valuable information to add. + +### Reversibility + +Your migration should be reversible. This is very important, as it should +be possible to downgrade in case of a vulnerability or bugs. + +In your migration, add a comment describing how the reversibility of the +migration was tested. + + +## Removing indices + +If you need to remove index, please add a condition like in following example: + +``` +remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) +``` + +## Adding indices + +If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. + +## Testing + +Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. + +Make sure your migration can be reversed. + +## Data migration + +Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper. + +Example with Arel: + +``` +users = Arel::Table.new(:users) +users.group(users[:user_id]).having(users[:id].count.gt(5)) + +#updtae other tables with this results +``` + +Example with plain SQL and `quote_string` helper: + +``` +select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| + tag_name = quote_string(tag["name"]) + duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} + origin_tag_id = duplicate_ids.first + duplicate_ids.delete origin_tag_id + + execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") + execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") +end +``` diff --git a/doc/install/installation.md b/doc/install/installation.md index ca25eaea799..badea4de214 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -62,7 +62,13 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev nodejs + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + +If you want to use Kerberos for user authentication, then install libkrb5-dev: + + sudo apt-get install libkrb5-dev + +**Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed @@ -189,9 +195,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-10-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-11-stable gitlab -**Note:** You can change `7-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -235,10 +241,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, useful when editing via web - # Edit user.email according to what is set in gitlab.yml - sudo -u git -H git config --global user.name "GitLab" - sudo -u git -H git config --global user.email "example@example.com" + # Configure Git global settings for git user, used when editing via web editor sudo -u git -H git config --global core.autocrlf input # Configure Redis connection settings @@ -276,17 +279,19 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws + sudo -u git -H bundle install --deployment --without development test mysql aws kerberos # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws + sudo -u git -H bundle install --deployment --without development test postgres aws kerberos + +**Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. ### Install GitLab Shell GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.2] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -294,6 +299,8 @@ GitLab Shell is an SSH access and repository management software developed speci **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. +**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". + ### Initialize Database and Activate Advanced Features sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index b67f793c591..904d5d7fee2 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,13 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. +An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. + +We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. + ## Configuring GitLab for LDAP integration To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 24f7b4bb4b4..8e2a602ec35 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -75,6 +75,7 @@ Now we can choose one or more of the Supported Providers below to continue confi - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) +- [SAML](saml.md) ## Enable OmniAuth for an Existing User diff --git a/doc/integration/saml.md b/doc/integration/saml.md new file mode 100644 index 00000000000..a8cc5c8f74a --- /dev/null +++ b/doc/integration/saml.md @@ -0,0 +1,77 @@ +# SAML OmniAuth Provider + +GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users. + +First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP: + +1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "saml", + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } + ``` + +1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value). + +1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options. + +1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP. + +1. Restart GitLab for the changes to take effect. + +1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'. + +To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance: + ``` + https://gitlab.example.com/users/auth/saml/metadata + ``` + +At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims). + +On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. + diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e95ddbb7578..9c7f723c06d 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -66,16 +66,26 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing ## URL auto-linking -GFM will autolink standard URLs you copy and paste into your text. So if you want to link to a URL (instead of a textural link), you can simply put the URL in verbatim and it will be turned into a link to that URL. +GFM will autolink almost any URL you copy and paste into your text. - http://www.google.com + * http://www.google.com + * https://google.com/ + * ftp://ftp.us.debian.org/debian/ + * smb://foo/bar/baz + * irc://irc.freenode.net/gitlab + * http://localhost:3000 -http://www.google.com +* http://www.google.com +* https://google.com/ +* ftp://ftp.us.debian.org/debian/ +* smb://foo/bar/baz +* irc://irc.freenode.net/gitlab +* http://localhost:3000 ## Code and Syntax Highlighting @@ -423,7 +433,7 @@ Quote break. You can also use raw HTML in your Markdown, and it'll mostly work pretty well. -See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements, as well as the `class`, and `id` attributes on all elements. +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements. ```no-highlight <dl> diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index bfef975024f..ae2d465e0c1 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -9,6 +9,8 @@ This archive will be saved in backup_path (see `config/gitlab.yml`). The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup. You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. +If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* + ``` # use this command if you've installed GitLab with the Omnibus package sudo gitlab-rake gitlab:backup:create @@ -150,11 +152,9 @@ If you have an installation from source, please consider backing up your `gitlab You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. -``` -# Omnibus package installation -sudo gitlab-rake gitlab:backup:restore +### Installation from source -# installation from source +``` bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` @@ -196,11 +196,45 @@ Restoring repositories: Deleting tmp directories...[DONE] ``` -## Configure cron to make daily backups +### Omnibus installations + +We will assume that you have installed GitLab from an omnibus package and run +`sudo gitlab-ctl reconfigure` at least once. + +First make sure your backup tar file is in `/var/opt/gitlab/backups`. + +```shell +sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ +``` + +Next, restore the backup by running the restore command. You need to specify the +timestamp of the backup you are restoring. + +```shell +# Stop processes that are connected to the database +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq + +# This command will overwrite the contents of your GitLab database! +sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 + +# Start GitLab +sudo gitlab-ctl start + +# Create satellites +sudo gitlab-rake gitlab:satellites:create + +# Check GitLab +sudo gitlab-rake gitlab:check SANITIZE=true +``` + +If there is a GitLab version mismatch between your backup tar file and the installed +version of GitLab, the restore command will abort with an error. Install a package for +the [required version](https://www.gitlab.com/downloads/archives/) and try again. -For Omnibus package installations, see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#scheduling-a-backup . +## Configure cron to make daily backups -For installation from source: +### For installation from source: ``` cd /home/git/gitlab sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups @@ -217,6 +251,32 @@ Add the following lines at the bottom: The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. This is recommended to reduce cron spam. +### For omnibus installations + +To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: + +``` +sudo su - +crontab -e +``` + +There, add the following line to schedule the backup for everyday at 2 AM: + +``` +0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1 +``` + +You may also want to set a limited lifetime for backups to prevent regular +backups using all your disk space. To do this add the following lines to +`/etc/gitlab/gitlab.rb` and reconfigure: + +``` +# limit backup lifetime to 7 days - 604800 seconds +gitlab_rails['backup_keep_time'] = 604800 +``` + +NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + ## Alternative backup strategies If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. @@ -239,3 +299,7 @@ Example: LVM snapshots + rsync If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. + +### Note +This documentation is for GitLab CE. +We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
\ No newline at end of file diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 41a994f3f68..2aca91d5371 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -47,7 +47,6 @@ Git: /usr/bin/git Runs the following rake tasks: -- `gitlab:env:check` - `gitlab:gitlab_shell:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -147,7 +146,7 @@ Do you want to continue (yes/no)? yes ## Clear redis cache -If for some reason the dashboard shows wrong information you might want to +If for some reason the dashboard shows wrong information you might want to clear Redis' cache. For Omnibus-packages: diff --git a/doc/release/monthly.md b/doc/release/monthly.md index cfe01896d8f..eb97f3cd7f6 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -9,7 +9,8 @@ The new release manager should create overall issue to track the progress. ## Release Manager -A release manager is selected that coordinates all releases the coming month, including the patch releases for previous releases. +A release manager is selected that coordinates all releases the coming month, +including the patch releases for previous releases. The release manager has to make sure all the steps below are done and delegated where necessary. This person should also make sure this document is kept up to date and issues are created and updated. @@ -29,7 +30,6 @@ All steps from issue template are explained below ``` Xth: (7 working days before the 22nd) -- [ ] Code freeze - [ ] Update the CE changelog (#LINK) - [ ] Update the EE changelog (#LINK) - [ ] Update the CI changelog (#LINK) @@ -79,10 +79,6 @@ Xth: (1 working day before the 22nd) - - - -## Code Freeze - -Stop merging code in master, except for important bug fixes - ## Update changelog Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is @@ -98,6 +94,8 @@ There are three changelogs that need to be updated: CE, EE and CI. Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre. +On creating the stable branches, notify the core team and developers. + ## QA Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. diff --git a/doc/security/README.md b/doc/security/README.md index 49dfa6eec76..473f3632dcd 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -4,3 +4,4 @@ - [Rack attack](rack_attack.md) - [Web Hooks and insecure internal web services](webhooks.md) - [Information exclusivity](information_exclusivity.md) +- [Reset your root password](reset_root_password.md)
\ No newline at end of file diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md new file mode 100644 index 00000000000..3c13f262677 --- /dev/null +++ b/doc/security/reset_root_password.md @@ -0,0 +1,40 @@ +# How to reset your root password + +Log into your server with root privileges. Then start a Ruby on Rails console. + +Start the console with this command: + +```bash +gitlab-rails console production +``` + +Wait until the console has loaded. + +There are multiple ways to find your user. You can search for email or username. + +```bash +user = User.where(id: 1).first +``` + +or + +```bash +user = User.find_by(email: 'admin@local.host') +``` + +Now you can change your password: + +```bash +user.password = 'secret_pass' +user.password_confirmation = 'secret_pass' +``` + +It's important that you change both password and password_confirmation to make it work. + +Don't forget to save the changes. + +```bash +user.save! +``` + +Exit the console and try to login with your new password.
\ No newline at end of file diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index f9b6d37d840..b0e4613cdef 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -6,6 +6,12 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser ## Hooks request example +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + **Project created:** ```json diff --git a/doc/update/6.x-or-7.x-to-7.10.md b/doc/update/6.x-or-7.x-to-7.11.md index 39e12f32d0e..b1daa648f1f 100644 --- a/doc/update/6.x-or-7.x-to-7.10.md +++ b/doc/update/6.x-or-7.x-to-7.11.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.10 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.10.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.11 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.11.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.10. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.11. ## Global issue numbers @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-10-stable +sudo -u git -H git checkout 7-11-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-10-stable-ee +sudo -u git -H git checkout 7-11-stable-ee ``` ## 4. Install additional packages @@ -91,7 +91,8 @@ sudo apt-get install logrotate # Install pkg-config and cmake, which is needed for the latest versions of rugged sudo apt-get install pkg-config cmake -# Install Kerberos header files, which are needed for GitLab EE Kerberos support +# If you want to use Kerberos with GitLab EE for user authentication, install Kerberos header files +# If you don't know what Kerberos is, you can assume you don't need it. sudo apt-get install libkrb5-dev # Install nodejs, javascript runtime required for assets @@ -126,7 +127,7 @@ sudo apt-get install nodejs ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.2 +sudo -u git -H git checkout v2.6.3 ``` ## 7. Install libs, migrations, etc. @@ -161,11 +162,11 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-10-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-11-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-11-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-11-stable/config/unicorn.rb.example but with your settings. * Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. * Copy rack attack middleware config @@ -181,8 +182,8 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-11-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-11-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ## 9. Start application diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md new file mode 100644 index 00000000000..79bc6de1e46 --- /dev/null +++ b/doc/update/7.10-to-7.11.md @@ -0,0 +1,103 @@ +# From 7.10 to 7.11 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-11-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-11-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 4. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-10-stable:config/gitlab.yml.example origin/7-11-stable:config/gitlab.yml.example +`````` + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.10) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.9 to 7.10](7.9-to-7.10.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 50941db25f6..2c43cf59c1f 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,7 +1,7 @@ # Migrating GitLab from MySQL to Postgres *Make sure you view this [guide from the `master` branch](../../../master/doc/update/mysql_to_postgresql.md) for the most up to date instructions.* -If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, import into Postgres and rebuild the indexes as described below. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. +If you are replacing MySQL with Postgres while keeping GitLab on the same server all you need to do is to export from MySQL, convert the resulting SQL file, and import it into Postgres. If you are also moving GitLab to another server, or if you are switching to omnibus-gitlab, you may want to use a GitLab backup file. The second part of this documents explains the procedure to do this. ## Export from MySQL and import into Postgres @@ -14,13 +14,11 @@ sudo service gitlab stop git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab cd mysql-postgresql-converter -mysqldump --compatible=postgresql --default-character-set=utf8 -r databasename.mysql -u root gitlabhq_production -p -python db_converter.py databasename.mysql databasename.psql +mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production -p +python db_converter.py gitlabhq_production.mysql gitlabhq_production.psql # Import the database dump as the application database user -sudo -u git psql -f databasename.psql -d gitlabhq_production - -# Rebuild indexes (see below) +sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production # Install gems for PostgreSQL (note: the line below states '--without ... mysql') sudo -u git -H bundle install --without development test mysql --deployment @@ -28,51 +26,6 @@ sudo -u git -H bundle install --without development test mysql --deployment sudo service gitlab start ``` -## Rebuild indexes - -The lanyrd database converter script does not preserve all indexes, so we have to recreate them ourselves after migrating from MySQL. It is not necessary to shut down GitLab for this process. - -### For non-omnibus installations - -On non-omnibus installations (distributed using Git) we retrieve the index declarations from version control using `git stash`. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab - -cd /home/git/gitlab - -# Stash changes to db/schema.rb to make sure we can find the right index statements -sudo -u git -H git stash - -# Generate add_index.rb -ruby /tmp/mysql-postgresql-converter/add_index_statements.rb db/schema.rb > /tmp/mysql-postgresql-converter/add_index.rb - -# Create the indexes -sudo -u git -H bundle exec rails runner -e production 'eval $stdin.read' < /tmp/mysql-postgresql-converter/add_index.rb -``` - -### For omnibus-gitlab installations - -On omnibus-gitlab we need to get the index declarations from a file called `schema.rb.bundled`. For versions older than 6.9, we need to download the file. - -``` -# Clone the database converter on your Postgres-backed GitLab server -cd /tmp -/opt/gitlab/embedded/bin/git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab -cd /tmp/mysql-postgresql-converter - -# Download schema.rb.bundled if necessary -test -e /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled || sudo /opt/gitlab/embedded/bin/curl -o /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled https://gitlab.com/gitlab-org/gitlab-ce/raw/v6.9.1/db/schema.rb - -# Generate add_index.rb -/opt/gitlab/embedded/bin/ruby add_index_statements.rb /opt/gitlab/embedded/service/gitlab-rails/db/schema.rb.bundled > add_index.rb - -# Create the indexes -/opt/gitlab/bin/gitlab-rails runner 'eval $stdin.read' < add_index.rb -``` - ## Converting a GitLab backup file from MySQL to Postgres **Note:** Please make sure to have Python 2.7.x (or higher) installed. diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index d23534d58b6..6854250dab7 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -1,5 +1,10 @@ # GitLab Upgrader -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* + +*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script. + +Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. + +*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version. @@ -24,13 +29,15 @@ If you have local changes to your GitLab repository the script will stash them a ## 2. Run GitLab upgrade tool -Note: GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). + +GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) cd /home/git/gitlab - sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' # to perform a non-interactive install (no user input required) you can add -y - # sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y + # sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y ## 3. Start application @@ -59,13 +66,15 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` You've read through the entire guide and probably already did all the steps one by one. -Here is a one line command with step 1 to 5 for the next time you upgrade: +Below is a one line command with step 1 to 5 for the next time you upgrade. + +Please replace X.X.X with the [latest GitLab release](https://packages.gitlab.com/gitlab/gitlab-ce). ```bash cd /home/git/gitlab; \ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \ sudo service gitlab stop; \ - sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y; \ + sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"vX.X.X"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y; \ cd /home/git/gitlab-shell; \ sudo -u git -H git fetch; \ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`; \ diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 851f50f5e9a..73717ffc7d6 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -12,6 +12,12 @@ If you send a web hook to an SSL endpoint [the certificate will not be verified] Triggered when you push to the repository except when pushing tags. +**Request header**: + +``` +X-Gitlab-Event: Push Hook +``` + **Request body:** ```json @@ -63,6 +69,13 @@ Triggered when you push to the repository except when pushing tags. Triggered when you create (or delete) tags to the repository. +**Request header**: + +``` +X-Gitlab-Event: Tag Push Hook +``` + + **Request body:** ```json @@ -92,6 +105,12 @@ Triggered when you create (or delete) tags to the repository. Triggered when a new issue is created or an existing issue was updated/closed/reopened. +**Request header**: + +``` +X-Gitlab-Event: Issue Hook +``` + **Request body:** ```json @@ -121,10 +140,295 @@ Triggered when a new issue is created or an existing issue was updated/closed/re } } ``` +## Comment events + +Triggered when a new comment is made on commits, merge requests, issues, and code snippets. +The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The +payload will also include information about the target of the comment. For example, +a comment on a issue will include the specific issue information under the `issue` key. +Valid target types: + +1. `commit` +2. `merge_request` +3. `issue` +4. `snippet` + +### Comment on commit + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +} +``` + +### Comment on merge request + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "locked_at": null, + "source": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "target": { + "name": "Gitlab Test", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10 + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21: 00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + } + } +} +``` + +### Comment on issue + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Adminstrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +} +``` + +### Comment on code snippet + + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +``` +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-09 02:40:38 UTC", + "updated_at": "2015-04-09 02:40:38 UTC", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +} +``` ## Merge request events -Triggered when a new merge request is created or an existing merge request was updated/merged/closed. +Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: Merge Request Hook +``` **Request body:** diff --git a/doc/workflow/2fa.png b/doc/workflow/2fa.png Binary files differnew file mode 100644 index 00000000000..bbf415210d5 --- /dev/null +++ b/doc/workflow/2fa.png diff --git a/doc/workflow/2fa_auth.png b/doc/workflow/2fa_auth.png Binary files differnew file mode 100644 index 00000000000..4a4fbe68984 --- /dev/null +++ b/doc/workflow/2fa_auth.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 7e996dc47d4..89005e51958 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -11,5 +11,8 @@ - [Migrating from SVN to GitLab](migrating_from_svn.md) - [Project importing from GitHub to GitLab](import_projects_from_github.md) - [Project importing from GitLab.com to your private GitLab instance](import_projects_from_gitlab_com.md) +- [Two-factor Authentication (2FA)](two_factor_authentication.md) - [Protected branches](protected_branches.md) +- [Change your time zone](timezone.md) +- [Keyboard shortcuts](shortcuts.md) - [Web Editor](web_editor.md) diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md new file mode 100644 index 00000000000..ffcb832cdd7 --- /dev/null +++ b/doc/workflow/shortcuts.md @@ -0,0 +1,5 @@ +# GitLab keyboard shortcuts + +You can see GitLab's keyboard shortcuts by using 'shift + ?' + +![Shortcuts](shortcuts.png)
\ No newline at end of file diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png Binary files differnew file mode 100644 index 00000000000..68756ed1f98 --- /dev/null +++ b/doc/workflow/shortcuts.png diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md new file mode 100644 index 00000000000..7e08c0e51ac --- /dev/null +++ b/doc/workflow/timezone.md @@ -0,0 +1,30 @@ +# Changing your time zone + +The global time zone configuration parameter can be changed in `config/gitlab.yml`: +``` + # time_zone: 'UTC' +``` + +Uncomment and customize if you want to change the default time zone of GitLab application. + +To see all available time zones, run `bundle exec rake time:zones:all`. + + +## Changing time zone in omnibus installations + +GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`. + +To update, add the time zone that best applies to your location. Here are two examples: +``` +gitlab_rails['time_zone'] = 'America/New_York' +``` +or +``` +gitlab_rails['time_zone'] = 'Europe/Brussels' +``` + +After you added this field, reconfigure and restart: +``` +gitlab-ctl reconfigure +gitlab-ctl restart +``` diff --git a/doc/workflow/two_factor_authentication.md b/doc/workflow/two_factor_authentication.md new file mode 100644 index 00000000000..8ac1ca4b351 --- /dev/null +++ b/doc/workflow/two_factor_authentication.md @@ -0,0 +1,65 @@ +# Two-factor Authentication (2FA) + +Two-factor Authentication (2FA) provides an additional level of security to your +GitLab account. Once enabled, in addition to supplying your username and +password to login, you'll be prompted for a code generated by an application on +your phone. + +By enabling 2FA, the only way someone other than you can log into your account +is to know your username and password *and* have access to your phone. + +## Enabling 2FA + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-factor Authentication**. + +![Two-factor setup](2fa.png) + +**On your phone:** + +1. Install a compatible application. We recommend [Google Authenticator]. +1. In the application, add a new entry in one of two ways: + * Scan the code with your phone's camera to add the entry automatically. + * Enter the details provided to add the entry manually. + +**In GitLab:** + +1. Enter the six-digit pin number from the entry on your phone into the **Pin + code** field. +1. Click **Submit**. + +If the pin you entered was correct, you'll see a message indicating that +Two-factor Authentication has been enabled, and you'll be presented with a list +of recovery codes. + +## Recovery Codes + +Should you ever lose access to your phone, you can use one of the ten provided +backup codes to login to your account. We suggest copying or printing them for +storage in a safe place. **Each code can be used only once** to log in to your +account. + +If you lose the recovery codes or just want to generate new ones, you can do so +from the **Profile Settings** > **Acount** page where you first enabled 2FA. + +## Logging in with 2FA Enabled + +Logging in with 2FA enabled is only slightly different than a normal login. +Enter your username and password credentials as you normally would, and you'll +be presented with a second prompt for an authentication code. Enter the pin from +your phone's application or a recovery code to log in. + +![Two-factor authentication on sign in](2fa_auth.png) + +## Disabling 2FA + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Acount**. +1. Click **Disable Two-factor Authentication**. + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en diff --git a/doc_styleguide.md b/doc_styleguide.md new file mode 100644 index 00000000000..670af765f3a --- /dev/null +++ b/doc_styleguide.md @@ -0,0 +1,22 @@ +# Documentation styleguide + +This styleguide recommends best practices to improve documentation and to keep it organized and easy to find. + +## Text + +* Make sure that the documentation is added in the correct directory and that there's a link to it somewhere useful. + +* Add only one H1 or title in each document, by adding '#' at the begining of it (when using markdown). For subtitles, use '##', '###' and so on. + +* Do not duplicate information. + +* Be brief and clear. + + +## When adding images to a document + +* Create a directory to store the images with the specific name of the document where the images belong. It could be in the same directory where the .md document that you're working on is located. + +* Images should have a specific, non-generic name that will differentiate them. + +* Keep all file names in lower case.
\ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 2e533ae9dd5..a73ccd0dba0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -107,7 +107,7 @@ The directories on data container are: ### Configure GitLab -These container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. +This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index df828a2a349..fe3f7f0bcd2 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -5,15 +5,14 @@ RUN apt-get update -q \ && DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends \ ca-certificates \ openssh-server \ - wget + wget \ + apt-transport-https # Download & Install GitLab -# If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. -RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab-ce_7.10.1~omnibus.2-1_amd64.deb \ - && dpkg -i $TMP_FILE \ - && rm -f $TMP_FILE +RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list +RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - +RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce # Manage SSHD through runit RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ diff --git a/docker/single/Dockerfile b/docker/single/Dockerfile index 8cdc24cf045..a6cbf131237 100644 --- a/docker/single/Dockerfile +++ b/docker/single/Dockerfile @@ -2,20 +2,18 @@ FROM ubuntu:14.04 MAINTAINER Sytse Sijbrandij # Install required packages -RUN apt-get update -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get install -yq --no-install-recommends \ +RUN apt-get update -q \ + && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ ca-certificates \ openssh-server \ - wget + wget \ + apt-transport-https # Download & Install GitLab -# If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. -RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab-ce_7.10.1~omnibus.2-1_amd64.deb \ - && dpkg -i $TMP_FILE \ - && rm -f $TMP_FILE +RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list +RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add - +RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce # Manage SSHD through runit RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ @@ -30,6 +28,7 @@ EXPOSE 80 22 # Copy assets COPY assets/wrapper /usr/local/bin/ +COPY assets/gitlab.rb /etc/gitlab/ # Wrapper to handle signal, trigger runit and reconfigure GitLab CMD ["/usr/local/bin/wrapper"] diff --git a/docker/single/assets/gitlab.rb b/docker/single/assets/gitlab.rb new file mode 100644 index 00000000000..ef84e7832d6 --- /dev/null +++ b/docker/single/assets/gitlab.rb @@ -0,0 +1,37 @@ +# External URL should be your Docker instance. +# By default, GitLab will use the Docker container hostname. +# Always use port 80 here to force the internal nginx to bind port 80, +# even if you intend to use another port in Docker. +# external_url "http://192.168.59.103/" + +# Prevent Postgres from trying to allocate 25% of total memory +postgresql['shared_buffers'] = '1MB' + +# Configure GitLab to redirect PostgreSQL logs to the data volume +postgresql['log_directory'] = '/var/log/gitlab/postgresql' + +# Some configuration of GitLab +# You can find more at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration +gitlab_rails['gitlab_email_from'] = 'gitlab@example.com' +gitlab_rails['gitlab_support_email'] = 'support@example.com' +gitlab_rails['time_zone'] = 'Europe/Paris' + +# SMTP settings +# You must use an external server, the Docker container does not install an SMTP server +gitlab_rails['smtp_enable'] = true +gitlab_rails['smtp_address'] = "smtp.example.com" +gitlab_rails['smtp_port'] = 587 +gitlab_rails['smtp_user_name'] = "user" +gitlab_rails['smtp_password'] = "password" +gitlab_rails['smtp_domain'] = "example.com" +gitlab_rails['smtp_authentication'] = "plain" +gitlab_rails['smtp_enable_starttls_auto'] = true + +# Enable LDAP authentication +# gitlab_rails['ldap_enabled'] = true +# gitlab_rails['ldap_host'] = 'ldap.example.com' +# gitlab_rails['ldap_port'] = 389 +# gitlab_rails['ldap_method'] = 'plain' # 'ssl' or 'plain' +# gitlab_rails['ldap_allow_username_or_email_login'] = false +# gitlab_rails['ldap_uid'] = 'uid' +# gitlab_rails['ldap_base'] = 'ou=users,dc=example,dc=com' diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature index 9df47eb51fd..33439cd1e85 100644 --- a/features/admin/deploy_keys.feature +++ b/features/admin/deploy_keys.feature @@ -8,11 +8,6 @@ Feature: Admin Deploy Keys When I visit admin deploy keys page Then I should see all public deploy keys - Scenario: Deploy Keys show - When I visit admin deploy keys page - And I click on first deploy key - Then I should see deploy key details - Scenario: Deploy Keys new When I visit admin deploy keys page And I click 'New Deploy Key' diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index cf4b8d7283b..e3c01db2ebb 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -24,7 +24,8 @@ Feature: Dashboard Group When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list - Then I should not see the "Leave" button for group "Owned" + When I click on the "Leave" button for group "Owned" + Then I should see the "Can not leave message" @javascript Scenario: Guest should be able to leave from group diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature index a71f6124d9c..47cf774094f 100644 --- a/features/project/deploy_keys.feature +++ b/features/project/deploy_keys.feature @@ -9,9 +9,10 @@ Feature: Project Deploy Keys Then I should see project deploy key Scenario: I should see project deploy keys - Given other project has deploy key + Given other projects have deploy keys When I visit project deploy keys page - Then I should see other project deploy key + Then I should see other project deploy key + And I should only see the same deploy key once Scenario: I should see public deploy keys Given public deploy key exists @@ -26,7 +27,7 @@ Feature: Project Deploy Keys And I should see newly created deploy key Scenario: I attach other project deploy key to project - Given other project has deploy key + Given other projects have deploy keys And I visit project deploy keys page When I click attach deploy key Then I should be on deploy keys page diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index d9fbb875c28..ad1160e3343 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -38,3 +38,15 @@ Feature: Project Forked Merge Requests Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" Then the target repository should be the original repository + + @javascript + Scenario: I see the users in the target project for a new merge request + Given I logout + And I sign in as an admin + And I have a project forked off of "Shop" called "Forked Shop" + Then I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + When I click "Assign to" dropdown" + Then I should see the target project ID in the input selector + And I should see the users from the target project ID diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 60caf783fe4..eb091c291e9 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -10,8 +10,8 @@ Feature: Project Merge Requests Then I should see "Bug NS-04" in merge requests And I should not see "Feature NS-03" in merge requests - Scenario: I should see closed merge requests - Given I click link "Closed" + Scenario: I should see rejected merge requests + Given I click link "Rejected" Then I should see "Feature NS-03" in merge requests And I should not see "Bug NS-04" in merge requests @@ -207,3 +207,11 @@ Feature: Project Merge Requests Then I should see that I am subscribed When I click button "Unsubscribe" Then I should see that I am unsubscribed + + @javascript + Scenario: I can change the target branch + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + When I click the "Target branch" dropdown + And I select a new target branch + Then I should see new target branch changes diff --git a/features/project/project.feature b/features/project/project.feature index ae28312a69a..56ae5c78d01 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -62,3 +62,14 @@ Feature: Project And I add project tags And I save project Then I should see project tags + + Scenario: I should not see "New Issue" or "New Merge Request" buttons + Given I disable issues and merge requests in project + When I visit project "Shop" page + Then I should not see "New Issue" button + And I should not see "New Merge Request" button + + Scenario: I should not see Project snippets + Given I disable snippets in project + When I visit project "Shop" page + Then I should not see "Snippets" button diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 977cd609a11..7a70f348754 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -69,6 +69,11 @@ Feature: Project Wiki And I click on the "Pages" button Then I should see non-escaped link in the pages list + @javascript @focus + Scenario: Creating an invalid new page + Given I create a New page with an invalid name + Then I should see an error message + @javascript Scenario: Edit Wiki page that has a path Given I create a New page with paths diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb index fb0b611762e..844837d177d 100644 --- a/features/steps/admin/deploy_keys.rb +++ b/features/steps/admin/deploy_keys.rb @@ -14,17 +14,6 @@ class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps end end - step 'I click on first deploy key' do - click_link DeployKey.are_public.first.title - end - - step 'I should see deploy key details' do - deploy_key = DeployKey.are_public.first - current_path.should == admin_deploy_key_path(deploy_key) - page.should have_content(deploy_key.title) - page.should have_content(deploy_key.key) - end - step 'I visit admin deploy key page' do visit admin_deploy_key_path(deploy_key) end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 8508b2a8096..bb1f2f444f9 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -23,8 +23,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I see prefilled new Merge Request page' do current_path.should == new_namespace_project_merge_request_path(@project.namespace, @project) find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should == "fix" - find("#merge_request_target_branch").value.should == "master" + find("input#merge_request_source_branch").value.should == "fix" + find("input#merge_request_target_branch").value.should == "master" end step 'user with name "John Doe" joined project "Shop"' do diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb index 8384df2fb59..aeea49320ff 100644 --- a/features/steps/dashboard/group.rb +++ b/features/steps/dashboard/group.rb @@ -60,4 +60,8 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps page.should have_content "Samurai" page.should have_content "Tokugawa Shogunate" end + + step 'I should see the "Can not leave message"' do + page.should have_content "You can not leave Owned group because you're the last owner" + end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 228b83e5fd0..84348d1709a 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -203,8 +203,8 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see group milestones index page with milestones' do page.should have_content('Version 7.2') page.should have_content('GL-113') - page.should have_link('2 Issues', href: group_milestone_path("owned", "version-7-2", title: "Version 7.2")) - page.should have_link('3 Merge Requests', href: group_milestone_path("owned", "gl-113", title: "GL-113")) + page.should have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2")) + page.should have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113")) end step 'I click on one group milestone' do diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 13e93618eb7..b6e03b549af 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -7,6 +7,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I should see global notifications settings' do - page.should have_content "Notifications Settings" + page.should have_content "Notifications" end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 791982d16c3..32888eb17d9 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -3,25 +3,27 @@ class Spinach::Features::Profile < Spinach::FeatureSteps include SharedPaths step 'I should see my profile info' do - page.should have_content "Profile Settings" + page.should have_content "This information will appear on your profile" end step 'I change my profile info' do - fill_in "user_skype", with: "testskype" - fill_in "user_linkedin", with: "testlinkedin" - fill_in "user_twitter", with: "testtwitter" - fill_in "user_website_url", with: "testurl" - fill_in "user_location", with: "Ukraine" - click_button "Save changes" + fill_in 'user_skype', with: 'testskype' + fill_in 'user_linkedin', with: 'testlinkedin' + fill_in 'user_twitter', with: 'testtwitter' + fill_in 'user_website_url', with: 'testurl' + fill_in 'user_location', with: 'Ukraine' + fill_in 'user_bio', with: 'I <3 GitLab' + click_button 'Save changes' @user.reload end step 'I should see new profile info' do - @user.skype.should == 'testskype' - @user.linkedin.should == 'testlinkedin' - @user.twitter.should == 'testtwitter' - @user.website_url.should == 'testurl' - find("#user_location").value.should == "Ukraine" + expect(@user.skype).to eq 'testskype' + expect(@user.linkedin).to eq 'testlinkedin' + expect(@user.twitter).to eq 'testtwitter' + expect(@user.website_url).to eq 'testurl' + expect(@user.bio).to eq 'I <3 GitLab' + find('#user_location').value.should == 'Ukraine' end step 'I change my avatar' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 30b1934b363..c888e82e207 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -12,7 +12,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I click atom feed link' do - click_link "Feed" + click_link "Commits Feed" end step 'I see commits atom feed' do diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 50e14513a7a..81d1182cd1b 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -45,10 +45,20 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end end - step 'other project has deploy key' do - @second_project = create :project, namespace: create(:group) + step 'other projects have deploy keys' do + @second_project = create(:project, namespace: create(:group)) @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) + + @third_project = create(:project, namespace: create(:group)) + @third_project.team << [current_user, :master] + create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) + end + + step 'I should only see the same deploy key once' do + within '.available-keys' do + page.should have_selector('ul li', count: 1) + end end step 'public deploy key exists' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 94d21d28a0c..ebfa102cee5 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -128,6 +128,21 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps page.should have_select("merge_request_target_project_id", selected: @project.path_with_namespace) end + step 'I click "Assign to" dropdown"' do + first('.ajax-users-select').click + end + + step 'I should see the target project ID in the input selector' do + expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]") + end + + step 'I should see the users from the target project ID' do + expect(page).to have_selector('.user-result', visible: true, count: 2) + users = page.all('.user-name') + users[0].text.should == 'Unassigned' + users[1].text.should == @project.users.first.name + end + # Verify a link is generated against the correct project def verify_commit_link(container_div, container_project) # This should force a wait for the javascript to execute diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index 4b135202593..d06905285fe 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -23,7 +23,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'I submit new hook' do - @url = Faker::Internet.uri("http") + @url = FFaker::Internet.uri("http") fill_in "hook_url", with: @url expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f67e6e3d8ca..4ca7cf5e5fe 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -19,8 +19,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_link "All" end - step 'I click link "Closed"' do - click_link "Closed" + step 'I click link "Rejected"' do + click_link "Rejected" end step 'I should see merge request "Wiki Feature"' do @@ -32,7 +32,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see closed merge request "Bug NS-04"' do merge_request = MergeRequest.find_by!(title: "Bug NS-04") merge_request.closed?.should be_true - page.should have_content "Closed by" + page.should have_content "Rejected by" end step 'I should see merge request "Bug NS-04"' do @@ -113,7 +113,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click on the Changes tab via Javascript' do - find('.diffs-tab').click + within '.merge-request-tabs' do + click_link 'Changes' + end + sleep 2 end @@ -202,7 +205,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see merged request' do within '.issue-box' do - page.should have_content "Merged" + page.should have_content "Accepted" end end @@ -302,6 +305,20 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps fill_in 'issue_search', with: "Fe" end + step 'I click the "Target branch" dropdown' do + first('.target_branch').click + end + + step 'I select a new target branch' do + select "feature", from: "merge_request_target_branch" + click_button 'Save' + end + + step 'I should see new target branch changes' do + page.should have_content 'From fix into feature' + page.should have_content 'Target branch changed from master to feature' + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 00706ab30e9..fcc15aacc21 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -102,4 +102,16 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should see project tags' do expect(find_field('Tags').value).to eq 'tag1, tag2' end + + step 'I should not see "New Issue" button' do + page.should_not have_link 'New Issue' + end + + step 'I should not see "New Merge Request" button' do + page.should_not have_link 'New Merge Request' + end + + step 'I should not see "Snippets" button' do + page.should_not have_link 'Snippets' + end end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 717132da45d..58cb0ceb3f1 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -133,6 +133,16 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps current_path.should include 'one/two/three' end + step 'I create a New page with an invalid name' do + click_on 'New Page' + fill_in 'Page slug', with: 'invalid name' + click_on 'Build' + end + + step 'I should see an error message' do + expect(page).to have_content "The page slug is invalid" + end + step 'I should see non-escaped link in the pages list' do page.should have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b60ac5e3423..3059c4ee041 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -14,6 +14,17 @@ module SharedProject @project.team << [@user, :master] end + step 'I disable snippets in project' do + @project.snippets_enabled = false + @project.save + end + + step 'I disable issues and merge requests in project' do + @project.issues_enabled = false + @project.merge_requests_enabled = false + @project.save + end + # Add another user to project "Shop" step 'I add a user to project "Shop"' do @project = Project.find_by(name: "Shop") diff --git a/features/support/env.rb b/features/support/env.rb index f34302721ed..d4a878ea4ce 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,7 +9,6 @@ end ENV['RAILS_ENV'] = 'test' require './config/environment' -require 'rspec' require 'rspec/expectations' require 'sidekiq/testing/inline' diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f768c750402..e88b6e31775 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -62,7 +62,7 @@ module API delete ":id" do group = find_group(params[:id]) authorize! :admin_group, group - group.destroy + DestroyGroupService.new(group, current_user).execute end # Transfer a project to the Group namespace diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 85e9081680d..1ebf9a1f022 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -243,7 +243,7 @@ module API end def secret_token - File.read(Rails.root.join('.gitlab_shell_secret')).chomp + File.read(Gitlab.config.gitlab_shell.secret_file).chomp end def handle_member_errors(errors) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index f98a17773e7..e38736fc28b 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -24,10 +24,6 @@ module API User.find_by(id: params[:user_id]) end - unless actor - return Gitlab::GitAccessStatus.new(false, 'No such user or key') - end - project_path = params[:project] # Check for *.wiki repositories. @@ -39,22 +35,14 @@ module API project = Project.find_with_namespace(project_path) - if project - access = - if wiki - Gitlab::GitAccessWiki.new(actor, project) - else - Gitlab::GitAccess.new(actor, project) - end - - status = access.check(params[:action], params[:changes]) - end + access = + if wiki + Gitlab::GitAccessWiki.new(actor, project) + else + Gitlab::GitAccess.new(actor, project) + end - if project && access.can_read_project? - status - else - Gitlab::GitAccessStatus.new(false, 'No such project') - end + access.check(params[:action], params[:changes]) end # diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 2216a12a87a..d835dce2ded 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -137,7 +137,6 @@ module API # Parameters: # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # source_branch - The source branch # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR @@ -148,10 +147,15 @@ module API # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :modify_merge_request, merge_request + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end + # Validate label names in advance if (errors = validate_label_params(params)).any? render_api_error!({ labels: errors }, 400) diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index b90ed6af5fb..50d3729449e 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,10 +1,7 @@ module API # namespaces API class Namespaces < Grape::API - before do - authenticate! - authenticated_as_admin! - end + before { authenticate! } resource :namespaces do # Get a namespaces list @@ -12,7 +9,11 @@ module API # Example Request: # GET /namespaces get do - @namespaces = Namespace.all + @namespaces = if current_user.admin + Namespace.all + else + current_user.namespaces + end @namespaces = @namespaces.search(params[:search]) if params[:search].present? @namespaces = paginate @namespaces diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index be9850367b9..ad4d2e65dfd 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -43,7 +43,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] @hook = user_project.hooks.new(attrs) @@ -73,7 +74,8 @@ module API :push_events, :issues_events, :merge_requests_events, - :tag_push_events + :tag_push_events, + :note_events ] if @hook.update_attributes attrs diff --git a/lib/api/projects.rb b/lib/api/projects.rb index e3fff79d68f..1f2251c9b9c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -22,7 +22,12 @@ module API projects = projects.search(params[:search]) end - projects.reorder(project_order_by => project_sort) + if params[:ci_enabled_first].present? + projects.includes(:gitlab_ci_service). + reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") + else + projects.reorder(project_order_by => project_sort) + end end def project_order_by diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 518964db50d..22b8f90dc5c 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -47,7 +47,7 @@ module API owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data) + @hook.execute(data, 'system_hooks') data end diff --git a/lib/api/users.rb b/lib/api/users.rb index 032a5d76e43..7d4c68c7412 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -194,7 +194,7 @@ module API user = User.find_by(id: params[:id]) if user - user.destroy + DeleteUserService.new.execute(user) else not_found!('User') end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 424541b4a04..6d0e30e916f 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -51,9 +51,9 @@ module Gitlab def protection_options { - "Not protected, developers and masters can (force) push and delete the branch" => PROTECTION_NONE, - "Partially protected, developers can also push but prevent all force pushes and deletion" => PROTECTION_DEV_CAN_PUSH, - "Fully protected, only masters can push and prevent all force pushes and deletion" => PROTECTION_FULL, + "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, + "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL, } end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb new file mode 100644 index 00000000000..bf33e5b1b1e --- /dev/null +++ b/lib/gitlab/asciidoc.rb @@ -0,0 +1,60 @@ +require 'asciidoctor' +require 'html/pipeline' + +module Gitlab + # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters + # the resulting HTML through HTML pipeline filters. + module Asciidoc + + # Provide autoload paths for filters to prevent a circular dependency error + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' + + DEFAULT_ADOC_ATTRS = [ + 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', + 'env-gitlab', 'source-highlighter=html-pipeline' + ].freeze + + # Public: Converts the provided Asciidoc markup into HTML. + # + # input - the source text in Asciidoc format + # context - a Hash with the template context: + # :commit + # :project + # :project_wiki + # :requested_path + # :ref + # asciidoc_opts - a Hash of options to pass to the Asciidoctor converter + # html_opts - a Hash of options for HTML output: + # :xhtml - output XHTML instead of HTML + # + def self.render(input, context, asciidoc_opts = {}, html_opts = {}) + asciidoc_opts = asciidoc_opts.reverse_merge( + safe: :secure, + backend: html_opts[:xhtml] ? :xhtml5 : :html5, + attributes: [] + ) + asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + + html = ::Asciidoctor.convert(input, asciidoc_opts) + + if context[:project] + result = HTML::Pipeline.new(filters).call(html, context) + + save_opts = html_opts[:xhtml] ? + Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0 + + html = result[:output].to_html(save_with: save_opts) + end + + html.html_safe + end + + private + + def self.filters + [ + Gitlab::Markdown::RelativeLinkFilter + ] + end + end +end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 050b5ba29dd..03cef30c97d 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,4 +1,3 @@ -require_relative 'rack_attack_helpers' require_relative 'shell_env' module Grack diff --git a/lib/gitlab/backend/rack_attack_helpers.rb b/lib/gitlab/backend/rack_attack_helpers.rb deleted file mode 100644 index 8538f3f6eca..00000000000 --- a/lib/gitlab/backend/rack_attack_helpers.rb +++ /dev/null @@ -1,31 +0,0 @@ -# rack-attack v4.2.0 doesn't yet support clearing of keys. -# Taken from https://github.com/kickstarter/rack-attack/issues/113 -class Rack::Attack::Allow2Ban - def self.reset(discriminator, options) - findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option" - - cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime) - cache.delete("#{key_prefix}:ban:#{discriminator}") - end -end - -class Rack::Attack::Cache - def reset_count(unprefixed_key, period) - epoch_time = Time.now.to_i - # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA - expires_in = period - (epoch_time % period) + 1 - key = "#{(epoch_time / period).to_i}:#{unprefixed_key}" - delete(key) - end - - def delete(unprefixed_key) - store.delete("#{prefix}:#{unprefixed_key}") - end -end - -class Rack::Attack::StoreProxy::RedisStoreProxy - def delete(key, options={}) - self.del(key) - rescue Redis::BaseError - end -end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 530f9d93de4..172d4902add 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -244,6 +244,16 @@ module Gitlab end end + # Check if such directory exists in repositories. + # + # Usage: + # exists?('gitlab') + # exists?('gitlab/cookies.git') + # + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end + protected def gitlab_shell_path @@ -264,10 +274,6 @@ module Gitlab File.join(repos_path, dir_name) end - def exists?(dir_name) - File.exists?(full_path(dir_name)) - end - def gitlab_shell_projects_path File.join(gitlab_shell_path, 'bin', 'gitlab-projects') end diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index ab184d95c05..aeec595782c 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -8,7 +8,7 @@ module Gitlab def closed_by_message(message) return [] if message.nil? - + closing_statements = message.scan(ISSUE_CLOSING_REGEX). map { |ref| ref[0] }.join(" ") diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index bc72b7528d5..c90184d31cf 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -31,8 +31,7 @@ module Gitlab def can_push_to_branch?(ref) return false unless user - if project.protected_branch?(ref) && - !(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user)) + if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref) user.can?(:push_code_to_protected_branches, project) else user.can?(:push_code, project) @@ -50,13 +49,25 @@ module Gitlab end def check(cmd, changes = nil) + unless actor + return build_status_object(false, "No user or key was provided.") + end + + if user && !user_allowed? + return build_status_object(false, "Your account has been blocked.") + end + + unless project && can_read_project? + return build_status_object(false, 'The project you were looking for could not be found.') + end + case cmd when *DOWNLOAD_COMMANDS download_access_check when *PUSH_COMMANDS push_access_check(changes) else - build_status_object(false, "Wrong command") + build_status_object(false, "The command you're trying to execute is not allowed.") end end @@ -64,7 +75,7 @@ module Gitlab if user user_download_access_check elsif deploy_key - deploy_key_download_access_check + build_status_object(true) else raise 'Wrong actor' end @@ -74,39 +85,27 @@ module Gitlab if user user_push_access_check(changes) elsif deploy_key - build_status_object(false, "Deploy key not allowed to push") + build_status_object(false, "Deploy keys are not allowed to push code.") else raise 'Wrong actor' end end def user_download_access_check - if user && user_allowed? && user.can?(:download_code, project) - build_status_object(true) - else - build_status_object(false, "You don't have access") + unless user.can?(:download_code, project) + return build_status_object(false, "You are not allowed to download code from this project.") end - end - def deploy_key_download_access_check - if can_read_project? - build_status_object(true) - else - build_status_object(false, "Deploy key not allowed to access this project") - end + build_status_object(true) end def user_push_access_check(changes) - unless user && user_allowed? - return build_status_object(false, "You don't have access") - end - if changes.blank? return build_status_object(true) end unless project.repository.exists? - return build_status_object(false, "Repository does not exist") + return build_status_object(false, "A repository for this project does not exist yet.") end changes = changes.lines if changes.kind_of?(String) @@ -136,11 +135,24 @@ module Gitlab :push_code end - if user.can?(action, project) - build_status_object(true) - else - build_status_object(false, "You don't have permission") + unless user.can?(action, project) + status = + case action + when :force_push_code_to_protected_branches + build_status_object(false, "You are not allowed to force push code to a protected branch on this project.") + when :remove_protected_branches + build_status_object(false, "You are not allowed to deleted protected branches from this project.") + when :push_code_to_protected_branches + build_status_object(false, "You are not allowed to push code to protected branches on this project.") + when :admin_project + build_status_object(false, "You are not allowed to change existing tags on this project.") + else # :push_code + build_status_object(false, "You are not allowed to push code to this project.") + end + return status end + + build_status_object(true) end def forced_push?(oldrev, newrev) diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 73d99b96202..8ba97184e69 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -4,7 +4,7 @@ module Gitlab if user.can?(:write_wiki, project) build_status_object(true) else - build_status_object(false, "You don't have access") + build_status_object(false, "You are not allowed to write to this project's wiki.") end end end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 960fb3849b4..16ff03c38d4 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -40,7 +40,7 @@ module Gitlab user.block unless user.blocked? false else - user.activate if user.blocked? + user.activate if user.blocked? && !ldap_config.block_auto_created_users true end else diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 63294aa54c0..fa9c0975bb8 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -1,5 +1,4 @@ require 'html/pipeline' -require 'task_list/filter' module Gitlab # Custom parser for GitLab-flavored Markdown @@ -12,21 +11,24 @@ module Gitlab autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter' autoload :EmojiFilter, 'gitlab/markdown/emoji_filter' autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter' + autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter' autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' + autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' + autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # project - the project + # options - options # html_options - extra options for the reference links as given to link_to - def gfm(text, project = @project, html_options = {}) - gfm_with_options(text, {}, project, html_options) + def gfm(text, options = {}, html_options = {}) + gfm_with_options(text, options, html_options) end # Public: Parse the provided text with GitLab-Flavored Markdown @@ -37,7 +39,7 @@ module Gitlab # :reference_only_path - Use relative path for reference links # project - the project # html_options - extra options for the reference links as given to link_to - def gfm_with_options(text, options = {}, project = @project, html_options = {}) + def gfm_with_options(text, options = {}, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str @@ -47,12 +49,17 @@ module Gitlab options.reverse_merge!( xhtml: false, - reference_only_path: true + reference_only_path: true, + project: @project, + current_user: current_user ) pipeline = HTML::Pipeline.new(filters) context = { + # SanitizationFilter + pipeline: options[:pipeline], + # EmojiFilter asset_root: Gitlab.config.gitlab.url, asset_host: Gitlab::Application.config.asset_host, @@ -61,10 +68,15 @@ module Gitlab no_header_anchors: options[:no_header_anchors], # ReferenceFilter - current_user: current_user, + current_user: options[:current_user], only_path: options[:reference_only_path], - project: project, - reference_class: html_options[:class] + project: options[:project], + reference_class: html_options[:class], + + # RelativeLinkFilter + ref: @ref, + requested_path: @path, + project_wiki: @project_wiki } result = pipeline.call(text, context) @@ -91,9 +103,11 @@ module Gitlab [ Gitlab::Markdown::SanitizationFilter, + Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, Gitlab::Markdown::UserReferenceFilter, Gitlab::Markdown::IssueReferenceFilter, @@ -104,7 +118,7 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, - TaskList::Filter + Gitlab::Markdown::TaskListFilter ] end end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index 8764f7e474f..61591a9914b 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -19,7 +19,7 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(COMMIT_RANGE_PATTERN) do |match| + text.gsub(CommitRange.reference_pattern) do |match| yield match, $~[:commit_range], $~[:project] end end @@ -30,13 +30,8 @@ module Gitlab @commit_map = {} end - # Pattern used to extract commit range references from text - # - # This pattern supports cross-project references. - COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>#{CommitRange::PATTERN})/ - def call - replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content| + replace_text_nodes_matching(CommitRange.reference_pattern) do |content| commit_range_link_filter(content) end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index b20b29f5d0c..f6932e76e70 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -19,20 +19,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(COMMIT_PATTERN) do |match| + text.gsub(Commit.reference_pattern) do |match| yield match, $~[:commit], $~[:project] end end - # Pattern used to extract commit references from text - # - # The SHA1 sum can be between 6 and 40 hex characters. - # - # This pattern supports cross-project references. - COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/ - def call - replace_text_nodes_matching(COMMIT_PATTERN) do |content| + replace_text_nodes_matching(Commit.reference_pattern) do |content| commit_link_filter(content) end end diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb index c436fabd658..66c256c5104 100644 --- a/lib/gitlab/markdown/cross_project_reference.rb +++ b/lib/gitlab/markdown/cross_project_reference.rb @@ -3,9 +3,6 @@ module Gitlab # Common methods for ReferenceFilters that support an optional cross-project # reference. module CrossProjectReference - NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR - PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})" - # Given a cross-project reference string, get the Project record # # Defaults to value of `context[:project]` if: diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb index 0fc3f4cca06..afd28dd8cf3 100644 --- a/lib/gitlab/markdown/external_issue_reference_filter.rb +++ b/lib/gitlab/markdown/external_issue_reference_filter.rb @@ -16,19 +16,16 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(ISSUE_PATTERN) do |match| + text.gsub(ExternalIssue.reference_pattern) do |match| yield match, $~[:issue] end end - # Pattern used to extract `JIRA-123` issue references from text - ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/ - def call # Early return if the project isn't using an external tracker return doc if project.nil? || project.default_issues_tracker? - replace_text_nodes_matching(ISSUE_PATTERN) do |content| + replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| issue_link_filter(content) end end @@ -51,7 +48,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{issue}</a>) + class="#{klass}">#{match}</a>) end end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb new file mode 100644 index 00000000000..c539e0fb823 --- /dev/null +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -0,0 +1,33 @@ +require 'html/pipeline/filter' + +module Gitlab + module Markdown + # HTML Filter to add a `rel="nofollow"` attribute to external links + # + class ExternalLinkFilter < HTML::Pipeline::Filter + def call + doc.search('a').each do |node| + next unless node.has_attribute?('href') + + link = node.attribute('href').value + + # Skip non-HTTP(S) links + next unless link.start_with?('http') + + # Skip internal links + next if link.start_with?(internal_url) + + node.set_attribute('rel', 'nofollow') + end + + doc + end + + private + + def internal_url + @internal_url ||= Gitlab.config.gitlab.url + end + end + end +end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index 1e885615163..dea04761ead 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -20,18 +20,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(ISSUE_PATTERN) do |match| + text.gsub(Issue.reference_pattern) do |match| yield match, $~[:issue].to_i, $~[:project] end end - # Pattern used to extract `#123` issue references from text - # - # This pattern supports cross-project references. - ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/ - def call - replace_text_nodes_matching(ISSUE_PATTERN) do |content| + replace_text_nodes_matching(Issue.reference_pattern) do |content| issue_link_filter(content) end end @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}##{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index a357f28458d..e022ca69c91 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -15,26 +15,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(LABEL_PATTERN) do |match| + text.gsub(Label.reference_pattern) do |match| yield match, $~[:label_id].to_i, $~[:label_name] end end - # Pattern used to extract label references from text - # - # TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad). - LABEL_PATTERN = %r{ - ~( - (?<label_id>\d+) | # Integer-based label ID, or - (?<label_name> - [A-Za-z0-9_-]+ | # String-based single-word label title - ['"][^&\?,]+['"] # String-based multi-word label surrounded in quotes - ) - ) - }x - def call - replace_text_nodes_matching(LABEL_PATTERN) do |content| + replace_text_nodes_matching(Label.reference_pattern) do |content| label_link_filter(content) end end @@ -84,11 +71,10 @@ module Gitlab # # Returns a Hash. def label_params(id, name) - if id > 0 - { id: id } + if name + { name: name.tr('"', '') } else - # TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding. - { name: name.tr('\'"', '') } + { id: id } end end end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 740d72abb36..80779819485 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -20,18 +20,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(MERGE_REQUEST_PATTERN) do |match| + text.gsub(MergeRequest.reference_pattern) do |match| yield match, $~[:merge_request].to_i, $~[:project] end end - # Pattern used to extract `!123` merge request references from text - # - # This pattern supports cross-project references. - MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/ - def call - replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content| + replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| merge_request_link_filter(content) end end @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}!#{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a4303d96bef..a84bacd3d4f 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/string/output_safety' -require 'html/pipeline' +require 'html/pipeline/filter' module Gitlab module Markdown @@ -25,12 +25,18 @@ module Gitlab ERB::Util.html_escape_once(html) end - # Don't look for references in text nodes that are children of these - # elements. - IGNORE_PARENTS = %w(pre code a style).to_set + def ignore_parents + @ignore_parents ||= begin + # Don't look for references in text nodes that are children of these + # elements. + parents = %w(pre code a style) + parents << 'blockquote' if context[:ignore_blockquotes] + parents.to_set + end + end def ignored_ancestry?(node) - has_ancestor?(node, IGNORE_PARENTS) + has_ancestor?(node, ignore_parents) end def project diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb new file mode 100644 index 00000000000..9de2b24a9da --- /dev/null +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -0,0 +1,128 @@ +require 'html/pipeline/filter' +require 'uri' + +module Gitlab + module Markdown + # HTML filter that "fixes" relative links to files in a repository. + # + # Context options: + # :commit + # :project + # :project_wiki + # :ref + # :requested_path + class RelativeLinkFilter < HTML::Pipeline::Filter + def call + return doc unless linkable_files? + + doc.search('a').each do |el| + process_link_attr el.attribute('href') + end + + doc.search('img').each do |el| + process_link_attr el.attribute('src') + end + + doc + end + + protected + + def linkable_files? + context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty? + end + + def process_link_attr(html_attr) + return if html_attr.blank? + + uri = URI(html_attr.value) + if uri.relative? && uri.path.present? + html_attr.value = rebuild_relative_uri(uri).to_s + end + rescue URI::Error + # noop + end + + def rebuild_relative_uri(uri) + file_path = relative_file_path(uri.path) + + uri.path = [ + relative_url_root, + context[:project].path_with_namespace, + path_type(file_path), + ref || 'master', # assume that if no ref exists we can point to master + file_path + ].compact.join('/').squeeze('/').chomp('/') + + uri + end + + def relative_file_path(path) + nested_path = build_nested_path(path, context[:requested_path]) + file_exists?(nested_path) ? nested_path : path + end + + # Covering a special case, when the link is referencing file in the same + # directory. + # If we are at doc/api/README.md and the README.md contains relative + # links like [Users](users.md), this takes the request + # path(doc/api/README.md) and replaces the README.md with users.md so the + # path looks like doc/api/users.md. + # If we are at doc/api and the README.md shown in below the tree view + # this takes the request path(doc/api) and adds users.md so the path + # looks like doc/api/users.md + def build_nested_path(path, request_path) + return request_path if path.empty? + return path unless request_path + + parts = request_path.split('/') + parts.pop if path_type(request_path) != 'tree' + parts.push(path).join('/') + end + + def file_exists?(path) + return false if path.nil? + repository.blob_at(current_sha, path).present? || + repository.tree(current_sha, path).entries.any? + end + + # Get the type of the given path + # + # path - String path to check + # + # Examples: + # + # path_type('doc/README.md') # => 'blob' + # path_type('doc/logo.png') # => 'raw' + # path_type('doc/api') # => 'tree' + # + # Returns a String + def path_type(path) + if repository.tree(current_sha, path).entries.any? + 'tree' + elsif repository.blob_at(current_sha, path).try(:image?) + 'raw' + else + 'blob' + end + end + + def current_sha + context[:commit].try(:id) || + ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha + end + + def relative_url_root + Gitlab.config.gitlab.relative_url_root.presence || '/' + end + + def ref + context[:ref] + end + + def repository + context[:project].try(:repository) + end + end + end +end diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index 9a154e0b2fe..74b3a8d274f 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -8,10 +8,37 @@ module Gitlab # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter def whitelist - whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST + # Descriptions are more heavily sanitized, allowing only a few elements. + # See http://git.io/vkuAN + if pipeline == :description + whitelist = LIMITED + whitelist[:elements] -= %w(pre code img ol ul li) + else + whitelist = super + end + + customize_whitelist(whitelist) + + whitelist + end + + private + + def pipeline + context[:pipeline] || :default + end + + def customized?(transformers) + transformers.last.source_location[0] == __FILE__ + end + + def customize_whitelist(whitelist) + # Only push these customizations once + return if customized?(whitelist[:transformers]) - # Allow `class` and `id` on all elements - whitelist[:attributes][:all].push('class', 'id') + # Allow code highlighting + whitelist[:attributes]['pre'] = %w(class) + whitelist[:attributes]['span'] = %w(class) # Allow table alignment whitelist[:attributes]['th'] = %w(style) @@ -23,6 +50,9 @@ module Gitlab # Remove `rel` attribute from `a` elements whitelist[:transformers].push(remove_rel) + # Remove `class` attribute from non-highlight spans + whitelist[:transformers].push(clean_spans) + whitelist end @@ -33,6 +63,17 @@ module Gitlab end end end + + def clean_spans + lambda do |env| + return unless env[:node_name] == 'span' + return unless env[:node].has_attribute?('class') + + unless has_ancestor?(env[:node], 'pre') + env[:node].remove_attribute('class') + end + end + end end end end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index 64a0a2696f7..174ba58af6c 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -20,18 +20,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(SNIPPET_PATTERN) do |match| + text.gsub(Snippet.reference_pattern) do |match| yield match, $~[:snippet].to_i, $~[:project] end end - # Pattern used to extract `$123` snippet references from text - # - # This pattern supports cross-project references. - SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/ - def call - replace_text_nodes_matching(SNIPPET_PATTERN) do |content| + replace_text_nodes_matching(Snippet.reference_pattern) do |content| snippet_link_filter(content) end end @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}$#{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/gitlab/markdown/task_list_filter.rb new file mode 100644 index 00000000000..c6eb2e2bf6d --- /dev/null +++ b/lib/gitlab/markdown/task_list_filter.rb @@ -0,0 +1,23 @@ +require 'task_list/filter' + +module Gitlab + module Markdown + # Work around a bug in the default TaskList::Filter that adds a `task-list` + # class to every list element, regardless of whether or not it contains a + # task list. + # + # This is a (hopefully) temporary fix, pending a new release of the + # task_list gem. + # + # See https://github.com/github/task_list/pull/60 + class TaskListFilter < TaskList::Filter + def add_css_class(node, *new_class_names) + if new_class_names.include?('task-list') + super if node.children.any? { |c| c['class'] == 'task-list-item' } + else + super + end + end + end + end +end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 28ec041b1d4..c9972957182 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -16,16 +16,13 @@ module Gitlab # # Returns a String replaced with the return of the block. def self.references_in(text) - text.gsub(USER_PATTERN) do |match| + text.gsub(User.reference_pattern) do |match| yield match, $~[:user] end end - # Pattern used to extract `@user` user references from text - USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/ - def call - replace_text_nodes_matching(USER_PATTERN) do |content| + replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) end end @@ -68,7 +65,8 @@ module Gitlab url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@all</a>) + text = User.reference_prefix + 'all' + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def link_to_namespace(namespace) @@ -86,7 +84,8 @@ module Gitlab url = urls.group_url(group, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@#{group}</a>) + text = Group.reference_prefix + group + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def link_to_user(user, namespace) @@ -94,7 +93,8 @@ module Gitlab url = urls.user_url(user, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@#{user}</a>) + text = User.reference_prefix + user + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def user_can_reference_group?(group) diff --git a/lib/gitlab/markdown_helper.rb b/lib/gitlab/markup_helper.rb index 5e3cfc0585b..f99be969d3e 100644 --- a/lib/gitlab/markdown_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -1,5 +1,5 @@ module Gitlab - module MarkdownHelper + module MarkupHelper module_function # Public: Determines if a given filename is compatible with GitHub::Markup. @@ -8,8 +8,10 @@ module Gitlab # # Returns boolean def markup?(filename) - filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki - .mediawiki .rst .adoc .asciidoc .asc)) + gitlab_markdown?(filename) || + asciidoc?(filename) || + filename.downcase.end_with?(*%w(.textile .rdoc .org .creole .wiki + .mediawiki .rst)) end # Public: Determines if a given filename is compatible with @@ -22,8 +24,17 @@ module Gitlab filename.downcase.end_with?(*%w(.mdown .md .markdown)) end + # Public: Determines if the given filename has AsciiDoc extension. + # + # filename - Filename string to check + # + # Returns boolean + def asciidoc?(filename) + filename.downcase.end_with?(*%w(.adoc .ad .asciidoc)) + end + def previewable?(filename) - gitlab_markdown?(filename) || markup?(filename) + markup?(filename) end end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 2f5c217d764..ba5caed6131 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -5,7 +5,7 @@ # module Gitlab module OAuth - class ForbiddenAction < StandardError; end + class SignupDisabledError < StandardError; end class User attr_accessor :auth_hash, :gl_user @@ -99,7 +99,7 @@ module Gitlab end def unauthorized_to_create - raise ForbiddenAction.new("Unauthorized to create user, signup disabled for #{auth_hash.provider}") + raise SignupDisabledError end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index e35f848fa6e..e836b05ff25 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,7 +1,7 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :project, :current_user, :references + attr_accessor :project, :current_user def initialize(project, current_user = nil) @project = project @@ -9,48 +9,31 @@ module Gitlab end def analyze(text) - @_text = text.dup + references.clear + @text = markdown.render(text.dup) end - def users - result = pipeline_result(:user) - result.uniq + %i(user label issue merge_request snippet commit commit_range).each do |type| + define_method("#{type}s") do + references[type] + end end - def labels - result = pipeline_result(:label) - result.uniq - end - - def issues - # TODO (rspeicher): What about external issues? - - result = pipeline_result(:issue) - result.uniq - end - - def merge_requests - result = pipeline_result(:merge_request) - result.uniq - end + private - def snippets - result = pipeline_result(:snippet) - result.uniq + def markdown + @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, GitlabMarkdownHelper::MARKDOWN_OPTIONS) end - def commits - result = pipeline_result(:commit) - result.uniq - end + def references + @references ||= Hash.new do |references, type| + type = type.to_sym + return references[type] if references.has_key?(type) - def commit_ranges - result = pipeline_result(:commit_range) - result.uniq + references[type] = pipeline_result(type).uniq + end end - private - # Instantiate and call HTML::Pipeline with a single reference filter type, # returning the result # @@ -65,11 +48,12 @@ module Gitlab project: project, current_user: current_user, # We don't actually care about the links generated - only_path: true + only_path: true, + ignore_blockquotes: true } pipeline = HTML::Pipeline.new([filter], context) - result = pipeline.call(@_text) + result = pipeline.call(@text) result[:references][filter_type] end diff --git a/lib/gitlab/satellite/files/delete_file_action.rb b/lib/gitlab/satellite/files/delete_file_action.rb deleted file mode 100644 index 0d37b9dea85..00000000000 --- a/lib/gitlab/satellite/files/delete_file_action.rb +++ /dev/null @@ -1,50 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class DeleteFileAction < FileAction - # Deletes file and creates a new commit for it - # - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - File.delete(file_path_in_satellite) - - # add removed file - repo.remove(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, ref) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb deleted file mode 100644 index 3cb9c0b5ecb..00000000000 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ /dev/null @@ -1,68 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - # GitLab server-side file update and commit - class EditFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding, new_branch = nil) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - begin - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - rescue Grit::Git::CommandFailed => ex - log_and_raise(CheckoutFailed, ex.message) - end - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # commit the changes - # will raise CommandFailed when commit fails - begin - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - rescue Grit::Git::CommandFailed => ex - log_and_raise(CommitFailed, ex.message) - end - - - target_branch = new_branch.present? ? "#{ref}:#{new_branch}" : ref - - # push commit back to bare repo - # will raise CommandFailed when push fails - begin - repo.git.push({ raise: true, timeout: true }, :origin, target_branch) - rescue Grit::Git::CommandFailed => ex - log_and_raise(PushFailed, ex.message) - end - - # everything worked - true - end - end - - private - - def log_and_raise(errorClass, message) - Gitlab::GitLogger.error(message) - raise(errorClass, message) - end - end - end -end diff --git a/lib/gitlab/satellite/files/file_action.rb b/lib/gitlab/satellite/files/file_action.rb deleted file mode 100644 index 6446b14568a..00000000000 --- a/lib/gitlab/satellite/files/file_action.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module Satellite - class FileAction < Action - attr_accessor :file_path, :ref - - def initialize(user, project, ref, file_path) - super user, project - @file_path = file_path - @ref = ref - end - - def safe_path?(path) - File.absolute_path(path) == path - end - - def write_file(abs_file_path, content, file_encoding = 'text') - if file_encoding == 'base64' - File.open(abs_file_path, 'wb') { |f| f.write(Base64.decode64(content)) } - else - File.open(abs_file_path, 'w') { |f| f.write(content) } - end - end - end - end -end diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb deleted file mode 100644 index 724dfa0d042..00000000000 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ /dev/null @@ -1,67 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class NewFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding, new_branch = nil) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - current_ref = - if @project.empty_repo? - # skip this step if we want to add first file to empty repo - Satellite::PARKING_BRANCH - else - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - ref - end - - file_path_in_satellite = File.join(repo.working_dir, file_path) - dir_name_in_satellite = File.dirname(file_path_in_satellite) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Create dir if not exists - FileUtils.mkdir_p(dir_name_in_satellite) - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # add new file - repo.add(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - target_branch = if new_branch.present? && !@project.empty_repo? - "#{ref}:#{new_branch}" - else - "#{current_ref}:#{ref}" - end - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, target_branch) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 75a3dfe37c3..06245374bc8 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -51,11 +51,23 @@ module Gitlab end def issues - Issue.where(project_id: limit_project_ids).full_search(query).order('updated_at DESC') + issues = Issue.where(project_id: limit_project_ids) + if query =~ /#(\d+)\z/ + issues = issues.where(iid: $1) + else + issues = issues.full_search(query) + end + issues.order('updated_at DESC') end def merge_requests - MergeRequest.in_projects(limit_project_ids).full_search(query).order('updated_at DESC') + merge_requests = MergeRequest.in_projects(limit_project_ids) + if query =~ /[#!](\d+)\z/ + merge_requests = merge_requests.where(iid: $1) + else + merge_requests = merge_requests.full_search(query) + end + merge_requests.order('updated_at DESC') end def default_scope diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index f33b2dedf4a..37232743325 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -7,7 +7,7 @@ module Gitlab GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i # Wait 30 seconds for running jobs to finish during graceful shutdown SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i - SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGTERM').to_s + SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s # Create a mutex used to ensure there will be only one thread waiting to # shut Sidekiq down diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 0570c2fbeb5..cf040971c6e 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -43,10 +43,15 @@ module Gitlab end def latest_version_raw + git_tags = fetch_git_tags + git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ } + git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) } + "v#{git_versions.sort.last.to_s}" + end + + def fetch_git_tags remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git)) - git_tags = remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) - git_tags = git_tags.select { |version| version =~ /v\d\.\d\.\d\Z/ } - last_tag = git_tags.last.match(/v\d\.\d\.\d/).to_s + remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) end def update_commands diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/omni_auth/request_forgery_protection.rb new file mode 100644 index 00000000000..3557522d3c9 --- /dev/null +++ b/lib/omni_auth/request_forgery_protection.rb @@ -0,0 +1,66 @@ +# Protects OmniAuth request phase against CSRF. + +module OmniAuth + # Based on ActionController::RequestForgeryProtection. + class RequestForgeryProtection + def initialize(env) + @env = env + end + + def request + @request ||= ActionDispatch::Request.new(@env) + end + + def session + request.session + end + + def reset_session + request.reset_session + end + + def params + request.params + end + + def call + verify_authenticity_token + end + + def verify_authenticity_token + if !verified_request? + Rails.logger.warn "Can't verify CSRF token authenticity" if Rails.logger + handle_unverified_request + end + end + + private + + def protect_against_forgery? + ApplicationController.allow_forgery_protection + end + + def request_forgery_protection_token + ApplicationController.request_forgery_protection_token + end + + def forgery_protection_strategy + ApplicationController.forgery_protection_strategy + end + + def verified_request? + !protect_against_forgery? || request.get? || request.head? || + form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == request.headers['X-CSRF-Token'] + end + + def handle_unverified_request + forgery_protection_strategy.new(self).handle_unverified_request + end + + # Sets the token value for the current session. + def form_authenticity_token + session[:_csrf_token] ||= SecureRandom.base64(32) + end + end +end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index bea66e6cdc1..2f7aff03c2a 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -7,9 +7,14 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML def initialize(template, color_scheme, options = {}) @template = template @color_scheme = color_scheme - @project = @template.instance_variable_get("@project") @options = options.dup + @options.reverse_merge!( + # Handled further down the line by Gitlab::Markdown::SanitizationFilter + escape_html: false, + project: @template.instance_variable_get("@project") + ) + super(options) end @@ -36,10 +41,6 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) - unless @template.instance_variable_get("@project_wiki") || @project.nil? - full_document = h.create_relative_links(full_document) - end - h.gfm_with_options(full_document, @options) end end diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index b066a1a6935..946902e2f6d 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -35,13 +35,14 @@ pid_path="$app_root/tmp/pids" socket_path="$app_root/tmp/sockets" web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +shell_path="/bin/bash" # Read configuration variable file if it is present test -f /etc/default/gitlab && . /etc/default/gitlab # Switch to the app_user if it is not he/she who is running the script. if [ "$USER" != "$app_user" ]; then - eval su - "$app_user" -c $(echo \")$0 "$@"$(echo \"); exit; + eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit; fi # Switch to the gitlab path, exit on failure. diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 9951bacedf5..cf7f4198cbf 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -29,3 +29,8 @@ web_server_pid_path="$pid_path/unicorn.pid" # sidekiq_pid_path defines the path in which to create the pid file for sidekiq # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" + +# shell_path defines the path of shell for "$app_user" in case you are using +# shell other than "bash" +# The default is "/bin/bash" +shell_path="/bin/bash" diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake index 3a225801ff2..5d4e0740373 100644 --- a/lib/tasks/brakeman.rake +++ b/lib/tasks/brakeman.rake @@ -1,6 +1,8 @@ desc 'Security check via brakeman' task :brakeman do - if system("brakeman --skip-files lib/backup/repository.rb -w3 -z") + # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge + # requests are welcome! + if system(*%W(brakeman --skip-files lib/backup/repository.rb -w3 -z)) puts 'Security check succeed' else puts 'Security check failed' diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 1a6303b6c82..75bd41f2838 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,7 +1,6 @@ namespace :gitlab do desc "GITLAB | Check the configuration of GitLab and its environment" - task check: %w{gitlab:env:check - gitlab:gitlab_shell:check + task check: %w{gitlab:gitlab_shell:check gitlab:sidekiq:check gitlab:ldap:check gitlab:app:check} @@ -14,6 +13,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "GitLab" + check_git_config check_database_config_exists check_database_is_not_sqlite check_migrations_are_up @@ -38,6 +38,36 @@ namespace :gitlab do # Checks ######################## + def check_git_config + print "Git configured with autocrlf=input? ... " + + options = { + "core.autocrlf" => "input" + } + + correct_options = options.map do |name, value| + run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value + end + + if correct_options.all? + puts "yes".green + else + print "Trying to fix Git error automatically. ..." + + if auto_fix_git_config(options) + puts "Success".green + else + puts "Failed".red + try_fixing_it( + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + end + end + end + def check_database_config_exists print "Database config exists? ... " @@ -298,58 +328,6 @@ namespace :gitlab do end end - - - namespace :env do - desc "GITLAB | Check the configuration of the environment" - task check: :environment do - warn_user_is_not_gitlab - start_checking "Environment" - - check_gitlab_git_config - - finished_checking "Environment" - end - - - # Checks - ######################## - - def check_gitlab_git_config - print "Git configured for #{gitlab_user} user? ... " - - options = { - "user.name" => "GitLab", - "user.email" => Gitlab.config.gitlab.email_from, - "core.autocrlf" => "input" - } - correct_options = options.map do |name, value| - run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value - end - - if correct_options.all? - puts "yes".green - else - print "Trying to fix Git error automatically. ..." - if auto_fix_git_config(options) - puts "Success".green - else - puts "Failed".red - try_fixing_it( - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") - ) - for_more_information( - see_installation_guide_section "GitLab" - ) - end - end - end - end - - - namespace :gitlab_shell do desc "GITLAB | Check the configuration of GitLab Shell" task check: :environment do diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 20abb2fa500..7c98ad3144f 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -35,7 +35,7 @@ namespace :gitlab do if project puts " * #{project.name} (#{repo_path}) exists" else - user = User.admins.first + user = User.admins.reorder("id").first project_params = { name: name, diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index e835d6cb9b7..afdaba11cb0 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -59,6 +59,9 @@ namespace :gitlab do # Launch installation process system(*%W(bin/install)) + + # (Re)create hooks + system(*%W(bin/create-hooks)) end # Required for debian packaging with PKGR: Setup .ssh/environment with diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 14a130be2ca..c95b6540ebc 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -118,9 +118,9 @@ namespace :gitlab do # Returns true if all subcommands were successfull (according to their exit code) # Returns false if any or all subcommands failed. def auto_fix_git_config(options) - if !@warned_user_not_gitlab && options['user.email'] != 'example@example.com' # default email should be overridden? + if !@warned_user_not_gitlab command_success = options.map do |name, value| - system(%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) end command_success.all? diff --git a/lib/tasks/jasmine.rake b/lib/tasks/jasmine.rake index 9e2cceffa19..ac307a9e929 100644 --- a/lib/tasks/jasmine.rake +++ b/lib/tasks/jasmine.rake @@ -7,6 +7,6 @@ task jasmine: ['jasmine:ci'] namespace :jasmine do task :ci do - Rake::Task['spec:javascript'].invoke + Rake::Task['teaspoon'].invoke end end diff --git a/lib/version_check.rb b/lib/version_check.rb new file mode 100644 index 00000000000..ea23344948c --- /dev/null +++ b/lib/version_check.rb @@ -0,0 +1,18 @@ +require "base64" + +# This class is used to build image URL to +# check if it is a new version for update +class VersionCheck + def data + { version: Gitlab::VERSION } + end + + def url + encoded_data = Base64.urlsafe_encode64(data.to_json) + "#{host}?gitlab_info=#{encoded_data}" + end + + def host + 'https://version.gitlab.com/check.png' + end +end diff --git a/public/404.html b/public/404.html index 867f193a98f..a0106bc760d 100644 --- a/public/404.html +++ b/public/404.html @@ -1,14 +1,15 @@ <!DOCTYPE html> <html> <head> - <title>The page you were looking for doesn't exist (404)</title> + <title>The page you're looking for could not be found (404)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <h1>404</h1> - <h3>The page you were looking for doesn't exist.</h3> + <h3>The page you're looking for could not be found.</h3> <hr/> - <p>You may have mistyped the address or the page may have moved.</p> + <p>Make sure the address is correct and that the page hasn't moved.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> </body> </html> diff --git a/public/422.html b/public/422.html index b6c37ac5386..026997b48e3 100644 --- a/public/422.html +++ b/public/422.html @@ -1,16 +1,16 @@ <!DOCTYPE html> <html> <head> - <title>The change you wanted was rejected (422)</title> + <title>The change you requested was rejected (422)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <!-- This file lives in public/422.html --> <h1>422</h1> - <div> - <h2>The change you wanted was rejected.</h2> - <p>Maybe you tried to change something you didn't have access to.</p> - </div> + <h3>The change you requested was rejected.</h3> + <hr /> + <p>Make sure you have access to the thing you tried to change.</p> + <p>Please contact your GitLab administrator if you think this is a mistake.</p> </body> </html> diff --git a/public/500.html b/public/500.html index c84b9e90e4b..08c11bbd05a 100644 --- a/public/500.html +++ b/public/500.html @@ -1,13 +1,14 @@ <!DOCTYPE html> <html> <head> - <title>We're sorry, but something went wrong (500)</title> + <title>Something went wrong (500)</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <h1>500</h1> - <h3>We're sorry, but something went wrong.</h3> + <h3>Whoops, something went wrong on our end.</h3> <hr/> + <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/502.html b/public/502.html index d171eccc927..9480a928439 100644 --- a/public/502.html +++ b/public/502.html @@ -6,8 +6,9 @@ </head> <body> <h1>502</h1> - <h3>GitLab is not responding.</h3> + <h3>Whoops, GitLab is taking too much time to respond.</h3> <hr/> + <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/deploy.html b/public/deploy.html index e41ed76573d..1a41b772f3c 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -1,11 +1,17 @@ <!DOCTYPE html> <html> <head> - <title>Deploy in progress. Please try again in a few minutes</title> + <title>Deploy in progress</title> <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> </head> + <body> - <h1><center><img src="/gitlab_logo.png"/></center>Deploy in progress</h1> - <h3>Please try again in a few minutes or contact your administrator.</h3> + <h1> + <img src="/gitlab_logo.png" /><br /> + Deploy in progress + </h1> + <h3>Please try again in a few minutes.</h3> + <hr/> + <p>Please contact your GitLab administrator if this problem persists.</p> </body> </html> diff --git a/public/static.css b/public/static.css index c6f92ac01d9..0a2b6060d48 100644 --- a/public/static.css +++ b/public/static.css @@ -2,18 +2,24 @@ body { color: #666; text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin:0; + margin: 0; width: 800px; margin: auto; font-size: 14px; } + h1 { font-size: 56px; line-height: 100px; font-weight: normal; color: #456; } -h2 { font-size: 24px; color: #666; line-height: 1.5em; } + +h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; +} h3 { color: #456; diff --git a/safe/public.pem b/safe/public.pem deleted file mode 100644 index c5ffe20a5c7..00000000000 --- a/safe/public.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnp2mUaLBoHFX127ysonX -OihiGpI4098eFfH1iAxpKHIof0vs0jFF05IUScNXJZ1U3w8G1U/unY/wGGa3NzAb -ZfDd22eOF6X2Gfiey6U4w9dFf0/UT5x1bphlpX357yh4O9oWWuNaWD062DTbOOsJ -U6UW2U/sZAu/QScys0Nw+gJ58t93hb4jFq+nO5IAQc6g4S8ek5YvIXOshFEpF2in -ZLbSYowx92+9GzfjvdQ7fk0Q2ssg0zfScVa6FY8n019osz0SC3wcSd/qicdfecpu -7oycpd9YDqk4lufE1qVMOsgE8OO4KXMrByz2f+T0p/bH9zdBa5HYylf1T7i60hIL -kQIDAQAB ------END PUBLIC KEY----- diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb new file mode 100644 index 00000000000..3dac134a731 --- /dev/null +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Groups::AvatarsController do + let(:user) { create(:user) } + let(:group) { create(:group, owner: user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + before do + sign_in(user) + end + + it 'destroy should remove avatar from DB' do + delete :destroy, group_id: group.path + @group = assigns(:group) + expect(@group.avatar.present?).to be_falsey + expect(@group).to be_valid + end +end diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb new file mode 100644 index 00000000000..ad5855df0a4 --- /dev/null +++ b/spec/controllers/profiles/avatars_controller_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Profiles::AvatarsController do + let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) } + + before do + sign_in(user) + controller.instance_variable_set(:@user, user) + end + + it 'destroy should remove avatar from DB' do + delete :destroy + @user = assigns(:user) + expect(@user.avatar.present?).to be_falsey + expect(@user).to be_valid + end +end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb new file mode 100644 index 00000000000..65415f21e55 --- /dev/null +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe Profiles::TwoFactorAuthsController do + before do + # `user` should be defined within the action-specific describe blocks + sign_in(user) + + allow(subject).to receive(:current_user).and_return(user) + end + + describe 'GET new' do + let(:user) { create(:user) } + + it 'generates otp_secret for user' do + expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once + + get :new + get :new # Second hit shouldn't re-generate it + end + + it 'assigns qr_code' do + code = double('qr code') + expect(subject).to receive(:build_qr_code).and_return(code) + + get :new + expect(assigns[:qr_code]).to eq code + end + end + + describe 'POST create' do + let(:user) { create(:user) } + let(:pin) { 'pin-code' } + + def go + post :create, pin_code: pin + end + + context 'with valid pin' do + before do + expect(user).to receive(:valid_otp?).with(pin).and_return(true) + end + + it 'sets otp_required_for_login' do + go + + user.reload + expect(user.otp_required_for_login).to eq true + end + + it 'presents plaintext codes for the user to save' do + expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) + + go + + expect(assigns[:codes]).to match_array %w(a b c) + end + + it 'renders create' do + go + expect(response).to render_template(:create) + end + end + + context 'with invalid pin' do + before do + expect(user).to receive(:valid_otp?).with(pin).and_return(false) + end + + it 'assigns error' do + go + expect(assigns[:error]).to eq 'Invalid pin code' + end + + it 'assigns qr_code' do + code = double('qr code') + expect(subject).to receive(:build_qr_code).and_return(code) + + go + expect(assigns[:qr_code]).to eq code + end + + it 'renders new' do + go + expect(response).to render_template(:new) + end + end + end + + describe 'POST codes' do + let(:user) { create(:user, :two_factor) } + + it 'presents plaintext codes for the user to save' do + expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) + + post :codes + expect(assigns[:codes]).to match_array %w(a b c) + end + + it 'persists the generated codes' do + post :codes + + user.reload + expect(user.otp_backup_codes).not_to be_empty + end + end + + describe 'DELETE destroy' do + let(:user) { create(:user, :two_factor) } + let!(:codes) { user.generate_otp_backup_codes! } + + it 'clears all 2FA-related fields' do + expect(user.otp_required_for_login).to eq true + expect(user.otp_backup_codes).not_to be_nil + expect(user.encrypted_otp_secret).not_to be_nil + + delete :destroy + + expect(user.otp_required_for_login).to eq false + expect(user.otp_backup_codes).to be_nil + expect(user.encrypted_otp_secret).to be_nil + end + + it 'redirects to profile_account_path' do + delete :destroy + + expect(response).to redirect_to(profile_account_path) + end + end +end diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb new file mode 100644 index 00000000000..e79b46a3504 --- /dev/null +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Projects::AvatarsController do + let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.team << [user, :developer] + controller.instance_variable_set(:@project, project) + end + + it 'destroy should remove avatar from DB' do + delete :destroy, namespace_id: project.namespace.id, project_id: project.id + expect(project.avatar.present?).to be_falsey + expect(project).to be_valid + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 19f2935f30e..9373a7af024 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,23 +2,23 @@ include ActionDispatch::TestProcess FactoryGirl.define do sequence :sentence, aliases: [:title, :content] do - Faker::Lorem.sentence + FFaker::Lorem.sentence end sequence :name do - Faker::Name.name + FFaker::Name.name end sequence :file_name do - Faker::Internet.user_name + FFaker::Internet.user_name end - sequence(:url) { Faker::Internet.uri('http') } + sequence(:url) { FFaker::Internet.uri('http') } factory :user, aliases: [:author, :assignee, :owner, :creator] do - email { Faker::Internet.email } + email { FFaker::Internet.email } name - sequence(:username) { |n| "#{Faker::Internet.user_name}#{n}" } + sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } password "12345678" confirmed_at { Time.now } confirmation_token { nil } @@ -28,6 +28,13 @@ FactoryGirl.define do admin true end + trait :two_factor do + before(:create) do |user| + user.otp_required_for_login = true + user.otp_secret = User.generate_otp_secret(32) + end + end + factory :omniauth_user do ignore do extern_uid '123456' @@ -115,12 +122,12 @@ FactoryGirl.define do factory :email do user email do - Faker::Internet.email('alias') + FFaker::Internet.email('alias') end factory :another_email do email do - Faker::Internet.email('another.alias') + FFaker::Internet.email('another.alias') end end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index f1c33461b55..e1009d5916e 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -25,15 +25,16 @@ FactoryGirl.define do note "Note" author - factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] - factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] - factory :note_on_merge_request, traits: [:on_merge_request] + factory :note_on_commit, traits: [:on_commit] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] + factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] - factory :note_on_project_snippet, traits: [:on_project_snippet] + factory :note_on_project_snippet, traits: [:on_project_snippet] + factory :system_note, traits: [:system] trait :on_commit do - project factory: :project + project commit_id RepoHelpers.sample_commit.id noteable_type "Commit" end @@ -43,7 +44,7 @@ FactoryGirl.define do end trait :on_merge_request do - project factory: :project + project noteable_id 1 noteable_type "MergeRequest" end @@ -58,6 +59,10 @@ FactoryGirl.define do noteable_type "Snippet" end + trait :system do + system true + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 25862614d28..00906e8087a 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -26,7 +26,7 @@ describe "Admin::Hooks", feature: true do describe "New Hook" do before do - @url = Faker::Internet.uri("http") + @url = FFaker::Internet.uri("http") visit admin_hooks_path fill_in "hook_url", with: @url expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1) diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 133beba7b98..16d1ca55f8d 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -11,7 +11,7 @@ describe "GitLab Flavored Markdown", feature: true do end before do - Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details") + Commit.any_instance.stub(title: "fix #{issue.to_reference}\n\nask #{fred.to_reference} for details") end let(:commit) { project.commit } @@ -25,25 +25,25 @@ describe "GitLab Flavored Markdown", feature: true do it "should render title in commits#index" do visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) - expect(page).to have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end it "should render title in repositories#branches" do visit namespace_project_branches_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -57,20 +57,20 @@ describe "GitLab Flavored Markdown", feature: true do author: @user, assignee: @user, project: project, - title: "fix ##{@other_issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{@other_issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render subject in issues#index" do visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render subject in issues#show" do visit namespace_project_issue_path(project.namespace, project, @issue) - expect(page).to have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render details in issues#show" do @@ -83,19 +83,19 @@ describe "GitLab Flavored Markdown", feature: true do describe "for merge requests" do before do - @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix ##{issue.iid}") + @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}") end it "should render title in merge_requests#index" do visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in merge_requests#show" do visit namespace_project_merge_request_path(project.namespace, project, @merge_request) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -104,26 +104,26 @@ describe "GitLab Flavored Markdown", feature: true do before do @milestone = create(:milestone, project: project, - title: "fix ##{issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render title in milestones#index" do visit namespace_project_milestones_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) - expect(page).to have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb new file mode 100644 index 00000000000..edc1c63a0aa --- /dev/null +++ b/spec/features/groups_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +feature 'Group' do + describe 'description' do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + before do + login_as(:admin) + end + + it 'parses Markdown' do + group.update_attribute(:description, 'This is **my** group') + visit path + expect(page).to have_css('.description > p > strong') + end + + it 'passes through html-pipeline' do + group.update_attribute(:description, 'This group is the :poop:') + visit path + expect(page).to have_css('.description > p > img') + end + + it 'sanitizes unwanted tags' do + group.update_attribute(:description, '# Group Description') + visit path + expect(page).not_to have_css('.description h1') + end + + it 'permits `rel` attribute on links' do + group.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.description a[rel]') + end + end +end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb new file mode 100644 index 00000000000..046a9f6191d --- /dev/null +++ b/spec/features/login_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +feature 'Login' do + describe 'with two-factor authentication' do + context 'with valid username/password' do + let(:user) { create(:user, :two_factor) } + + before do + login_with(user) + expect(page).to have_content('Two-factor Authentication') + end + + def enter_code(code) + fill_in 'Two-factor authentication code', with: code + click_button 'Verify code' + end + + it 'does not show a "You are already signed in." error message' do + enter_code(user.current_otp) + expect(page).not_to have_content('You are already signed in.') + end + + context 'using one-time code' do + it 'allows login with valid code' do + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + + it 'blocks login with invalid code' do + enter_code('foo') + expect(page).to have_content('Invalid two-factor code') + end + + it 'allows login with invalid code, then valid code' do + enter_code('foo') + expect(page).to have_content('Invalid two-factor code') + + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + end + + context 'using backup code' do + let(:codes) { user.generate_otp_backup_codes! } + + before do + expect(codes.size).to eq 10 + + # Ensure the generated codes get saved + user.save + end + + context 'with valid code' do + it 'allows login' do + enter_code(codes.sample) + expect(current_path).to eq root_path + end + + it 'invalidates the used code' do + expect { enter_code(codes.sample) }. + to change { user.reload.otp_backup_codes.size }.by(-1) + end + end + + context 'with invalid code' do + it 'blocks login' do + code = codes.sample + expect(user.invalidate_otp_backup_code!(code)).to eq true + + user.save! + expect(user.reload.otp_backup_codes.size).to eq 9 + + enter_code(code) + expect(page).to have_content('Invalid two-factor code.') + end + end + end + end + end + + describe 'without two-factor authentication' do + let(:user) { create(:user) } + + it 'allows basic login' do + login_with(user) + expect(current_path).to eq root_path + end + + it 'does not show a "You are already signed in." error message' do + login_with(user) + expect(page).not_to have_content('You are already signed in.') + end + + it 'blocks invalid login' do + user = create(:user, password: 'not-the-default') + + login_with(user) + expect(page).to have_content('Invalid email or password.') + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 1746ce128e4..902968cebcb 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -18,11 +18,13 @@ require 'erb' # -> `gfm_with_options` helper # -> HTML::Pipeline # -> Sanitize +# -> RelativeLink # -> Emoji # -> Table of Contents # -> Autolinks # -> Rinku (http, https, ftp) # -> Other schemes +# -> ExternalLink # -> References # -> TaskList # -> `html_safe` @@ -60,12 +62,16 @@ describe 'GitLab Markdown' do @feat.teardown end - # Given a header ID, goes to that element's parent (the header), then to its - # second sibling (the body). + # Given a header ID, goes to that element's parent (the header itself), then + # its next sibling element (the body). def get_section(id) @doc.at_css("##{id}").parent.next_element end + # Sometimes it can be useful to see the parsed output of the Markdown document + # for debugging. Uncomment this block to write the output to + # tmp/capybara/markdown_spec.html. + # # it 'writes to a file' do # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| # file.puts @md @@ -119,18 +125,18 @@ describe 'GitLab Markdown' do describe 'HTML::Pipeline' do describe 'SanitizationFilter' do it 'uses a permissive whitelist' do - expect(@doc).to have_selector('b#manual-b') - expect(@doc).to have_selector('em#manual-em') - expect(@doc).to have_selector("code#manual-code") + expect(@doc).to have_selector('b:contains("b tag")') + expect(@doc).to have_selector('em:contains("em tag")') + expect(@doc).to have_selector('code:contains("code tag")') expect(@doc).to have_selector('kbd:contains("s")') expect(@doc).to have_selector('strike:contains(Emoji)') - expect(@doc).to have_selector('img#manual-img') - expect(@doc).to have_selector('br#manual-br') - expect(@doc).to have_selector('hr#manual-hr') + expect(@doc).to have_selector('img[src*="smile.png"]') + expect(@doc).to have_selector('br') + expect(@doc).to have_selector('hr') end it 'permits span elements' do - expect(@doc).to have_selector('span#span-class-light.light') + expect(@doc).to have_selector('span:contains("span tag")') end it 'permits table alignment' do @@ -144,13 +150,12 @@ describe 'GitLab Markdown' do end it 'removes `rel` attribute from links' do - expect(@doc).to have_selector('a#a-rel-nofollow') - expect(@doc).not_to have_selector('a#a-rel-nofollow[rel]') + body = get_section('sanitizationfilter') + expect(body).not_to have_selector('a[rel="bookmark"]') end it "removes `href` from `a` elements if it's fishy" do - expect(@doc).to have_selector('a#a-href-javascript') - expect(@doc).not_to have_selector('a#a-href-javascript[href]') + expect(@doc).not_to have_selector('a[href*="javascript"]') end end @@ -228,11 +233,24 @@ describe 'GitLab Markdown' do %w(code a kbd).each do |elem| it "ignores links inside '#{elem}' element" do - expect(@doc.at_css("#{elem}#autolink-#{elem}").child).to be_text + body = get_section('autolinkfilter') + expect(body).not_to have_selector("#{elem} a") end end end + describe 'ExternalLinkFilter' do + let(:links) { get_section('externallinkfilter').next_element } + + it 'adds nofollow to external link' do + expect(links.css('a').first.to_html).to match 'nofollow' + end + + it 'ignores internal link' do + expect(links.css('a').last.to_html).not_to match 'nofollow' + end + end + describe 'ReferenceFilter' do it 'handles references in headers' do header = @doc.at_css('#reference-filters-eg-1').parent @@ -344,13 +362,13 @@ class MarkdownFeature end def commit - @commit ||= project.repository.commit + @commit ||= project.commit end def commit_range unless @commit_range - commit2 = project.repository.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}") + commit2 = project.commit('HEAD~3') + @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) end @commit_range @@ -376,11 +394,6 @@ class MarkdownFeature @xproject end - # Shortcut to "cross-reference/project" - def xref - xproject.path_with_namespace - end - def xissue @xissue ||= create(:issue, project: xproject) end @@ -394,13 +407,13 @@ class MarkdownFeature end def xcommit - @xcommit ||= xproject.repository.commit + @xcommit ||= xproject.commit end def xcommit_range unless @xcommit_range - xcommit2 = xproject.repository.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}") + xcommit2 = xproject.commit('HEAD~2') + @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) end @xcommit_range diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb new file mode 100644 index 00000000000..a34efce09ef --- /dev/null +++ b/spec/features/password_reset_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +feature 'Password reset' do + def forgot_password + click_on 'Forgot your password?' + fill_in 'Email', with: user.email + click_button 'Reset password' + user.reload + end + + def get_reset_token + mail = ActionMailer::Base.deliveries.last + body = mail.body.encoded + body.scan(/reset_password_token=(.+)\"/).flatten.first + end + + def reset_password(password = 'password') + visit edit_user_password_path(reset_password_token: get_reset_token) + + fill_in 'New password', with: password + fill_in 'Confirm new password', with: password + click_button 'Change your password' + end + + describe 'with two-factor authentication' do + let(:user) { create(:user, :two_factor) } + + it 'requires login after password reset' do + visit root_path + + forgot_password + reset_password + + expect(page).to have_content("Your password was changed successfully.") + expect(page).not_to have_content("You are now signed in.") + expect(current_path).to eq new_user_session_path + end + end + + describe 'without two-factor authentication' do + let(:user) { create(:user) } + + it 'automatically logs in after password reset' do + visit root_path + + forgot_password + reset_password + + expect(current_path).to eq root_path + expect(page).to have_content("Your password was changed successfully. You are now signed in.") + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index cae11be7cdd..f8eea70ec4a 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,32 +1,57 @@ require 'spec_helper' -describe "Projects", feature: true, js: true do - before { login_as :user } +feature 'Project' do + describe 'description' do + let(:project) { create(:project) } + let(:path) { namespace_project_path(project.namespace, project) } - describe "DELETE /projects/:id" do before do - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :master] - visit edit_namespace_project_path(@project.namespace, @project) + login_as(:admin) end - it "should remove project" do - expect { remove_project }.to change {Project.count}.by(-1) + it 'parses Markdown' do + project.update_attribute(:description, 'This is **my** project') + visit path + expect(page).to have_css('.project-home-desc > p > strong') + end + + it 'passes through html-pipeline' do + project.update_attribute(:description, 'This project is the :poop:') + visit path + expect(page).to have_css('.project-home-desc > p > img') + end + + it 'sanitizes unwanted tags' do + project.update_attribute(:description, '# Project Description') + visit path + expect(page).not_to have_css('.project-home-desc h1') + end + + it 'permits `rel` attribute on links' do + project.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.project-home-desc a[rel]') end + end + + describe 'removal', js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } - it 'should delete the project from disk' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{@project.path_with_namespace}/) - ).twice + before do + login_with(user) + project.team << [user, :master] + visit edit_namespace_project_path(project.namespace, project) + end - remove_project + it 'should remove project' do + expect { remove_project }.to change {Project.count}.by(-1) end end def remove_project click_link "Remove project" - fill_in 'confirm_name_input', with: @project.path + fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 69bac387d20..db20b23f87d 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -26,37 +26,37 @@ describe IssuesFinder do context 'scope: all' do it 'should filter by all' do params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues.size).to eq(3) end it 'should filter by assignee id' do params = { scope: "all", assignee_id: user.id, state: 'opened' } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues.size).to eq(2) end it 'should filter by author id' do params = { scope: "all", author_id: user2.id, state: 'opened' } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues).to eq([issue3]) end it 'should filter by milestone id' do params = { scope: "all", milestone_title: milestone.title, state: 'opened' } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues).to eq([issue1]) end it 'should be empty for unauthorized user' do params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(nil, params) + issues = IssuesFinder.new(nil, params).execute expect(issues.size).to be_zero end it 'should not include unauthorized issues' do params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new.execute(user2, params) + issues = IssuesFinder.new(user2, params).execute expect(issues.size).to eq(2) expect(issues).not_to include(issue1) expect(issues).to include(issue2) @@ -67,13 +67,13 @@ describe IssuesFinder do context 'personal scope' do it 'should filter by assignee' do params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues.size).to eq(2) end it 'should filter by project' do params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new.execute(user, params) + issues = IssuesFinder.new(user, params).execute expect(issues.size).to eq(1) end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 8536377a7f0..bc385fd0d69 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -20,13 +20,13 @@ describe MergeRequestsFinder do describe "#execute" do it 'should filter by scope' do params = { scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new.execute(user, params) + merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(2) end it 'should filter by project' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } - merge_requests = MergeRequestsFinder.new.execute(user, params) + merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(1) end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index bc023ecf793..02ab46c905a 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -54,36 +54,34 @@ After the Markdown has been turned into HTML, it gets passed through... ### SanitizationFilter -GitLab uses <a href="http://git.io/vfW8a" class="sanitize" id="sanitize-link">HTML::Pipeline::SanitizationFilter</a> +GitLab uses <a href="http://git.io/vfW8a">HTML::Pipeline::SanitizationFilter</a> to sanitize the generated HTML, stripping dangerous or unwanted tags. Its default whitelist is pretty permissive. Check it: -<b id="manual-b">This text is bold</b> and <em id="manual-em">this text is emphasized</em>. +<b>b tag</b> and <em>em tag</em>. -<code id="manual-code">echo "Hello, world!"</code> +<code>code tag</code> Press <kbd>s</kbd> to search. -<strike>Emoji</strike> Plain old images! <img -src="http://www.emoji-cheat-sheet.com/graphics/emojis/smile.png" width="20" -height="20" id="manual-img" /> +<strike>Emoji</strike> Plain old images! <img src="http://www.emoji-cheat-sheet.com/graphics/emojis/smile.png" width="20" height="20" /> Here comes a line break: -<br id="manual-br" /> +<br /> And a horizontal rule: -<hr id="manual-hr" /> +<hr /> As permissive as it is, we've allowed even more stuff: -<span class="light" id="span-class-light">Span elements</span> +<span>span tag</span> -<a href="#" rel="nofollow" id="a-rel-nofollow">This is a link with a defined rel attribute, which should be removed</a> +<a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a> -<a href="javascript:alert('Hi')" id="a-href-javascript">This is a link trying to be sneaky. It gets its link removed entirely.</a> +<a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a> ### Escaping @@ -125,65 +123,72 @@ These are all plain text that should get turned into links: But it shouldn't autolink text inside certain tags: -- <code id="autolink-code">http://about.gitlab.com/</code> -- <a id="autolink-a">http://about.gitlab.com/</a> -- <kbd id="autolink-kbd">http://about.gitlab.com/</kbd> +- <code>http://about.gitlab.com/</code> +- <a>http://about.gitlab.com/</a> +- <kbd>http://about.gitlab.com/</kbd> -### Reference Filters (e.g., #<%= issue.iid %>) +### ExternalLinkFilter -References should be parseable even inside _!<%= merge_request.iid %>_ emphasis. +External links get a `rel="nofollow"` attribute: + +- [Google](https://google.com/) +- [GitLab Root](<%= Gitlab.config.gitlab.url %>) + +### Reference Filters (e.g., <%= issue.to_reference %>) + +References should be parseable even inside _<%= merge_request.to_reference %>_ emphasis. #### UserReferenceFilter - All: @all -- User: @<%= user.username %> -- Group: @<%= group.name %> -- Ignores invalid: @fake_user -- Ignored in code: `@<%= user.username %>` -- Ignored in links: [Link to @<%= user.username %>](#user-link) +- User: <%= user.to_reference %> +- Group: <%= group.to_reference %> +- Ignores invalid: <%= User.reference_prefix %>fake_user +- Ignored in code: `<%= user.to_reference %>` +- Ignored in links: [Link to <%= user.to_reference %>](#user-link) #### IssueReferenceFilter -- Issue: #<%= issue.iid %> -- Issue in another project: <%= xref %>#<%= xissue.iid %> -- Ignored in code: `#<%= issue.iid %>` -- Ignored in links: [Link to #<%= issue.iid %>](#issue-link) +- Issue: <%= issue.to_reference %> +- Issue in another project: <%= xissue.to_reference(project) %> +- Ignored in code: `<%= issue.to_reference %>` +- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) #### MergeRequestReferenceFilter -- Merge request: !<%= merge_request.iid %> -- Merge request in another project: <%= xref %>!<%= xmerge_request.iid %> -- Ignored in code: `!<%= merge_request.iid %>` -- Ignored in links: [Link to !<%= merge_request.iid %>](#merge-request-link) +- Merge request: <%= merge_request.to_reference %> +- Merge request in another project: <%= xmerge_request.to_reference(project) %> +- Ignored in code: `<%= merge_request.to_reference %>` +- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) #### SnippetReferenceFilter -- Snippet: $<%= snippet.id %> -- Snippet in another project: <%= xref %>$<%= xsnippet.id %> -- Ignored in code: `$<%= snippet.id %>` -- Ignored in links: [Link to $<%= snippet.id %>](#snippet-link) +- Snippet: <%= snippet.to_reference %> +- Snippet in another project: <%= xsnippet.to_reference(project) %> +- Ignored in code: `<%= snippet.to_reference %>` +- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) #### CommitRangeReferenceFilter -- Range: <%= commit_range %> -- Range in another project: <%= xref %>@<%= xcommit_range %> -- Ignored in code: `<%= commit_range %>` -- Ignored in links: [Link to <%= commit_range %>](#commit-range-link) +- Range: <%= commit_range.to_reference %> +- Range in another project: <%= xcommit_range.to_reference(project) %> +- Ignored in code: `<%= commit_range.to_reference %>` +- Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link) #### CommitReferenceFilter -- Commit: <%= commit.id %> -- Commit in another project: <%= xref %>@<%= xcommit.id %> -- Ignored in code: `<%= commit.id %>` -- Ignored in links: [Link to <%= commit.id %>](#commit-link) +- Commit: <%= commit.to_reference %> +- Commit in another project: <%= xcommit.to_reference(project) %> +- Ignored in code: `<%= commit.to_reference %>` +- Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) #### LabelReferenceFilter -- Label by ID: ~<%= simple_label.id %> -- Label by name: ~<%= simple_label.name %> -- Label by name in quotes: ~"<%= label.name %>" -- Ignored in code: `~<%= simple_label.name %>` -- Ignored in links: [Link to ~<%= simple_label.id %>](#label-link) +- Label by ID: <%= simple_label.to_reference %> +- Label by name: <%= Label.reference_prefix %><%= simple_label.name %> +- Label by name in quotes: <%= label.to_reference(:name) %> +- Ignored in code: `<%= simple_label.to_reference %>` +- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) ### Task Lists diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index d4cf6540080..3307ac776fc 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -261,12 +261,26 @@ describe ApplicationHelper do end end - describe 'markup_render' do + describe 'render_markup' do let(:content) { 'Noël' } it 'should preserve encoding' do expect(content.encoding.name).to eq('UTF-8') expect(render_markup('foo.rst', content).encoding.name).to eq('UTF-8') end + + it "should delegate to #markdown when file name corresponds to Markdown" do + expect(self).to receive(:gitlab_markdown?).with('foo.md').and_return(true) + expect(self).to receive(:markdown).and_return('NOEL') + + expect(render_markup('foo.md', content)).to eq('NOEL') + end + + it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do + expect(self).to receive(:asciidoc?).with('foo.adoc').and_return(true) + expect(self).to receive(:asciidoc).and_return('NOEL') + + expect(render_markup('foo.adoc', content)).to eq('NOEL') + end end end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb new file mode 100644 index 00000000000..e49e4e6d5d8 --- /dev/null +++ b/spec/helpers/blob_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe BlobHelper do + describe 'highlight' do + let(:blob_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:split_content) { blob_content.split("\n") } + + it 'should return plaintext for unknown lexer context' do + result = highlight(blob_name, no_context_content, nowrap: true, continue: false) + expect(result).to eq('<span id="LC1" class="line">:type "assem"))</span>') + end + + it 'should highlight single block' do + expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> +<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>] + + expect(highlight(blob_name, blob_content, nowrap: true, continue: false)).to eq(expected) + end + + it 'should highlight continued blocks' do + # Both lines have LC1 as ID since formatter doesn't support continue at the moment + expected = [ + '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>', + '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>' + ] + + result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } + expect(result).to eq(expected) + end + end +end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index dd4c1d645e2..e0be2df0e5e 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -106,6 +106,16 @@ describe DiffHelper do end end + describe 'unfold_class' do + it 'returns empty on false' do + expect(unfold_class(false)).to eq('') + end + + it 'returns a class on true' do + expect(unfold_class(true)).to eq('unfold js-unfold') + end + end + describe 'diff_line_content' do it 'should return non breaking space when line is empty' do diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb new file mode 100644 index 00000000000..7a3e38d7e63 --- /dev/null +++ b/spec/helpers/emails_helper_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe EmailsHelper do + describe 'password_reset_token_valid_time' do + def validate_time_string(time_limit, expected_string) + Devise.reset_password_within = time_limit + expect(password_reset_token_valid_time).to eq(expected_string) + end + + context 'when time limit is less than 2 hours' do + it 'should display the time in hours using a singular unit' do + validate_time_string(1.hour, '1 hour') + end + end + + context 'when time limit is 2 or more hours' do + it 'should display the time in hours using a plural unit' do + validate_time_string(2.hours, '2 hours') + end + end + + context 'when time limit contains fractions of an hour' do + it 'should round down to the nearest hour' do + validate_time_string(96.minutes, '1 hour') + end + end + + context 'when time limit is 24 or more hours' do + it 'should display the time in days using a singular unit' do + validate_time_string(24.hours, '1 day') + end + end + + context 'when time limit is 2 or more days' do + it 'should display the time in days using a plural unit' do + validate_time_string(2.days, '2 days') + end + end + + context 'when time limit contains fractions of a day' do + it 'should round down to the nearest day' do + validate_time_string(57.hours, '2 days') + end + end + end +end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 2f67879efdc..bbb434638ce 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -21,12 +21,12 @@ describe GitlabMarkdownHelper do describe "#gfm" do it "should forward HTML options to links" do - expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')). + expect(gfm("Fixed in #{commit.id}", { project: @project }, class: 'foo')). to have_selector('a.gfm.foo') end describe "referencing multiple objects" do - let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } + let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" } it "should link to the merge request" do expected = namespace_project_merge_request_path(project.namespace, project, merge_request) @@ -50,7 +50,7 @@ describe GitlabMarkdownHelper do let(:issues) { create_list(:issue, 2, project: project) } it 'should handle references nested in links with all the text' do - actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) + actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) doc = Nokogiri::HTML.parse(actual) # Make sure we didn't create invalid markup @@ -63,7 +63,7 @@ describe GitlabMarkdownHelper do # First issue link expect(doc.css('a')[1].attr('href')). to eq namespace_project_issue_path(project.namespace, project, issues[0]) - expect(doc.css('a')[1].text).to eq "##{issues[0].iid}" + expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link expect(doc.css('a')[2].attr('href')).to eq commit_path @@ -72,7 +72,7 @@ describe GitlabMarkdownHelper do # Second issue link expect(doc.css('a')[3].attr('href')). to eq namespace_project_issue_path(project.namespace, project, issues[1]) - expect(doc.css('a')[3].text).to eq "##{issues[1].iid}" + expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link expect(doc.css('a')[4].attr('href')).to eq commit_path @@ -90,82 +90,15 @@ describe GitlabMarkdownHelper do end it "escapes HTML passed in as the body" do - actual = "This is a <h1>test</h1> - see ##{issues[0].iid}" + actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" expect(link_to_gfm(actual, commit_path)). to match('<h1>test</h1>') end - end - - describe "#markdown" do - # TODO (rspeicher): These belong in a relative link filter spec - context 'relative links' do - context 'with a valid repository' do - before do - @repository = project.repository - @ref = 'markdown' - end - - it "should handle relative urls for a file in master" do - actual = "[GitLab API doc](doc/api/README.md)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should handle relative urls for a file in master with an anchor" do - actual = "[GitLab API doc](doc/api/README.md#section)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md#section\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should not handle relative urls for the current file with an anchor" do - actual = "[GitLab API doc](#section)\n" - expected = "<p><a href=\"#section\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should handle relative urls for a directory in master" do - actual = "[GitLab API doc](doc/api)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should handle absolute urls" do - actual = "[GitLab](https://www.gitlab.com)\n" - expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should handle relative urls in reference links for a file in master" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should handle relative urls in reference links for a directory in master" do - actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n" - expect(markdown(actual)).to match(expected) - end - - it "should not handle malformed relative urls in reference links for a file in master" do - actual = "[GitLab readme]: doc/api/README.md\n" - expected = "" - expect(markdown(actual)).to match(expected) - end - end - context 'with an empty repository' do - before do - @project = create(:empty_project) - @repository = @project.repository - end - - it "should not touch relative urls" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - end + it 'ignores reference links when they are the entire body' do + text = issues[0].to_reference + act = link_to_gfm(text, '/foo') + expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) end end @@ -183,6 +116,14 @@ describe GitlabMarkdownHelper do helper.render_wiki_content(@wiki) end + it "should use Asciidoctor for asciidoc files" do + allow(@wiki).to receive(:format).and_return(:asciidoc) + + expect(helper).to receive(:asciidoc).with('wiki content') + + helper.render_wiki_content(@wiki) + end + it "should use the Gollum renderer for all other file types" do allow(@wiki).to receive(:format).and_return(:rdoc) formatted_content_stub = double('formatted_content') diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0b7e3b1d11f..0c8d06b7059 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -1,6 +1,70 @@ require 'spec_helper' describe LabelsHelper do - it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') } - it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') } + describe 'link_to_label' do + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } + + context 'with @project set' do + before do + @project = project + end + + it 'uses the instance variable' do + expect(label).not_to receive(:project) + link_to_label(label) + end + end + + context 'without @project set' do + it "uses the label's project" do + expect(label).to receive(:project).and_return(project) + link_to_label(label) + end + end + + context 'with a named project argument' do + it 'uses the provided project' do + arg = double('project') + expect(arg).to receive(:namespace).and_return('foo') + expect(arg).to receive(:to_param).and_return('foo') + + link_to_label(label, project: arg) + end + + it 'takes precedence over other types' do + @project = project + expect(@project).not_to receive(:namespace) + expect(label).not_to receive(:project) + + arg = double('project', namespace: 'foo', to_param: 'foo') + link_to_label(label, project: arg) + end + end + + context 'with block' do + it 'passes the block to link_to' do + link = link_to_label(label) { 'Foo' } + expect(link).to match('Foo') + end + end + + context 'without block' do + it 'uses render_colored_label as the link content' do + expect(self).to receive(:render_colored_label). + with(label).and_return('Foo') + expect(link_to_label(label)).to match('Foo') + end + end + end + + describe 'text_color_for_bg' do + it 'uses light text on dark backgrounds' do + expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') + end + + it 'uses dark text on light backgrounds' do + expect(text_color_for_bg('#EEEEEE')).to eq('#333333') + end + end end diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee new file mode 100644 index 00000000000..4ceac619422 --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js.coffee @@ -0,0 +1,12 @@ +#= require extensions/array + +describe 'Array extensions', -> + describe 'first', -> + it 'returns the first item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.first()).toBe(0) + + describe 'last', -> + it 'returns the last item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee new file mode 100644 index 00000000000..b10e16b7d01 --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js.coffee @@ -0,0 +1,34 @@ +#= require extensions/jquery + +describe 'jQuery extensions', -> + describe 'disable', -> + beforeEach -> + fixture.set '<input type="text" />' + + it 'adds the disabled attribute', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveAttr('disabled', 'disabled') + + it 'adds the disabled class', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveClass('disabled') + + describe 'enable', -> + beforeEach -> + fixture.set '<input type="text" disabled="disabled" class="disabled" />' + + it 'removes the disabled attribute', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveAttr('disabled') + + it 'removes the disabled class', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml new file mode 100644 index 00000000000..42ab4aa68b1 --- /dev/null +++ b/spec/javascripts/fixtures/issuable.html.haml @@ -0,0 +1,2 @@ +%form.js-main-target-form + %textarea#note_note diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml new file mode 100644 index 00000000000..0aecc7334fd --- /dev/null +++ b/spec/javascripts/fixtures/issue_note.html.haml @@ -0,0 +1,12 @@ +%ul + %li.note + .js-task-list-container + .note-text + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + .note-edit-form + %form + %textarea.js-task-list-field + \- [ ] Task List Item diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml new file mode 100644 index 00000000000..db5abe0cae3 --- /dev/null +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.issue-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-issue-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml new file mode 100644 index 00000000000..c4329b8f94a --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.merge-request-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-merge-request-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml new file mode 100644 index 00000000000..e867e4de2b9 --- /dev/null +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -0,0 +1,9 @@ +.zennable + %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } + .zen-backdrop + %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'} + %a.zen-enter-link{tabindex: '-1'} + %i.fa.fa-expand + Edit in fullscreen + %a.zen-leave-link + %i.fa.fa-compress diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 13b25862f57..268e4c68c31 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -1,34 +1,20 @@ -#= require jquery -#= require jasmine-fixture #= require issue describe 'Issue', -> describe 'task lists', -> - selectors = { - container: '.issue-details .description.js-task-list-container' - item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}' - form: 'form.js-issue-update[action="/foo"]' - close: 'a.btn-close' - } + fixture.preload('issues_show.html') beforeEach -> - $container = affix(selectors.container) - - # # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) - - # Task lists don't get initialized unless this button exists. Not ideal. - $container.append(affix(selectors.close)) - - # This form is used to get the `update` URL. Not ideal. - $container.append(affix(selectors.form)) - + fixture.load('issues_show.html') @issue = new Issue() + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + it 'submits an ajax request on tasklist:changed', -> - spyOn($, 'ajax').and.callFake (req) -> + spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PATCH') expect(req.url).toBe('/foo') expect(req.data.issue.description).not.toBe(null) diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee index 3ebc4a4eed5..a4735af0343 100644 --- a/spec/javascripts/merge_request_spec.js.coffee +++ b/spec/javascripts/merge_request_spec.js.coffee @@ -1,34 +1,23 @@ -#= require jquery -#= require jasmine-fixture #= require merge_request +window.disableButtonIfEmptyField = -> null + describe 'MergeRequest', -> describe 'task lists', -> - selectors = { - container: '.merge-request-details .description.js-task-list-container' - item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}' - form: 'form.js-merge-request-update[action="/foo"]' - close: 'a.btn-close' - } + fixture.preload('merge_requests_show.html') beforeEach -> - $container = affix(selectors.container) - - # # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) - - # Task lists don't get initialized unless this button exists. Not ideal. - $container.append(affix(selectors.close)) + fixture.load('merge_requests_show.html') + @merge = new MergeRequest({}) - # This form is used to get the `update` URL. Not ideal. - $container.append(affix(selectors.form)) + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() - @merge = new MergeRequest({}) + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') it 'submits an ajax request on tasklist:changed', -> - spyOn($, 'ajax').and.callFake (req) -> + spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PATCH') expect(req.url).toBe('/foo') expect(req.data.merge_request.description).not.toBe(null) diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index de2e8e7f6c8..050b6e362c6 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,5 +1,3 @@ -#= require jquery -#= require jasmine-fixture #= require notes window.gon = {} @@ -7,21 +5,18 @@ window.disableButtonIfEmptyField = -> null describe 'Notes', -> describe 'task lists', -> - selectors = { - container: 'li.note .js-task-list-container' - item: '.note-text ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.note-edit-form form textarea.js-task-list-field{- [ ] Task List Item}' - } + fixture.preload('issue_note.html') beforeEach -> - $container = affix(selectors.container) - - # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) + fixture.load('issue_note.html') + $('form').on 'submit', (e) -> e.preventDefault() @notes = new Notes() + it 'modifies the Markdown field', -> + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + it 'submits the form on tasklist:changed', -> submitted = false $('form').on 'submit', (e) -> submitted = true; e.preventDefault() diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee index 57dcc2161d3..a01ad7140dd 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js.coffee +++ b/spec/javascripts/shortcuts_issuable_spec.js.coffee @@ -1,10 +1,10 @@ -#= require jquery -#= require jasmine-fixture - #= require shortcuts_issuable describe 'ShortcutsIssuable', -> + fixture.preload('issuable.html') + beforeEach -> + fixture.load('issuable.html') @shortcut = new ShortcutsIssuable() describe '#replyWithSelectedText', -> @@ -14,7 +14,6 @@ describe 'ShortcutsIssuable', -> beforeEach -> @selector = 'form.js-main-target-form textarea#note_note' - affix(@selector) describe 'with empty selection', -> it 'does nothing', -> diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee new file mode 100644 index 00000000000..47b41dd2c81 --- /dev/null +++ b/spec/javascripts/spec_helper.coffee @@ -0,0 +1,46 @@ +# PhantomJS (Teaspoons default driver) doesn't have support for +# Function.prototype.bind, which has caused confusion. Use this polyfill to +# avoid the confusion. + +#= require support/bind-poly + +# You can require your own javascript files here. By default this will include +# everything in application, however you may get better load performance if you +# require the specific files that are being used in the spec that tests them. + +#= require jquery +#= require bootstrap +#= require underscore + +# Teaspoon includes some support files, but you can use anything from your own +# support path too. + +# require support/jasmine-jquery-1.7.0 +# require support/jasmine-jquery-2.0.0 +#= require support/jasmine-jquery-2.1.0 +# require support/sinon +# require support/your-support-file + +# Deferring execution + +# If you're using CommonJS, RequireJS or some other asynchronous library you can +# defer execution. Call Teaspoon.execute() after everything has been loaded. +# Simple example of a timeout: + +# Teaspoon.defer = true +# setTimeout(Teaspoon.execute, 1000) + +# Matching files + +# By default Teaspoon will look for files that match +# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +# and it'll be included in the default suite automatically. If you want to +# customize suites, check out the configuration in teaspoon_env.rb + +# Manifest + +# If you'd rather require your spec files manually (to control order for +# instance) you can disable the suite matcher in the configuration and use this +# file as a manifest. + +# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js index ee90892eb48..dbafe782b77 100644 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/stat_graph_contributors_util_spec.js @@ -118,9 +118,11 @@ describe("ContributorsStatGraphUtil", function () { describe("#add_author", function () { it("adds an author field to the collection", function () { var fake_author = { author_name: "Author", author_email: 'fake@email.com' } - var fake_collection = {} - ContributorsStatGraphUtil.add_author(fake_author, fake_collection) - expect(fake_collection[fake_author.author_name].author_name).toEqual("Author") + var fake_author_collection = {} + var fake_email_collection = {} + ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection) + expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author") + expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author") }) }) diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml deleted file mode 100644 index 168c9618643..00000000000 --- a/spec/javascripts/support/jasmine.yml +++ /dev/null @@ -1,15 +0,0 @@ -# path to parent directory of spec_files -# relative path from Rails.root -# -# Alternatively accept an array of directory to include external spec files -# spec_dir: -# - spec/javascripts -# - ../engine/spec/javascripts -# -# defaults to spec/javascripts -spec_dir: spec/javascripts - -# list of file expressions to include as specs into spec runner -# relative path from spec_dir -spec_files: - - "**/*[Ss]pec.{js.coffee,js,coffee}" diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb deleted file mode 100644 index 4d73aec5a31..00000000000 --- a/spec/javascripts/support/jasmine_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -#Use this file to set/override Jasmine configuration options -#You can remove it if you don't need it. -#This file is loaded *after* jasmine.yml is interpreted. -# -#Example: using a different boot file. -#Jasmine.configure do |config| -# config.boot_dir = '/absolute/path/to/boot_dir' -# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } -#end -# -#Example: prevent PhantomJS auto install, uses PhantomJS already on your path. -#Jasmine.configure do |config| -# config.prevent_phantom_js_auto_install = true -#end -# diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee new file mode 100644 index 00000000000..1f4ea58ad48 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js.coffee @@ -0,0 +1,52 @@ +#= require zen_mode + +describe 'ZenMode', -> + fixture.preload('zen_mode.html') + + beforeEach -> + fixture.load('zen_mode.html') + + # Stub Dropzone.forElement(...).enable() + spyOn(Dropzone, 'forElement').and.callFake -> + enable: -> true + + @zen = new ZenMode() + + # Set this manually because we can't actually scroll the window + @zen.scroll_position = 456 + + # Ohmmmmmmm + enterZen = -> + $('.zen-toggle-comment').prop('checked', true).trigger('change') + + # Wh- what was that?! + exitZen = -> + $('.zen-toggle-comment').prop('checked', false).trigger('change') + + describe 'on enter', -> + it 'pauses Mousetrap', -> + spyOn(Mousetrap, 'pause') + enterZen() + expect(Mousetrap.pause).toHaveBeenCalled() + + describe 'in use', -> + beforeEach -> + enterZen() + + it 'exits on Escape', -> + $(document).trigger(jQuery.Event('keydown', {keyCode: 27})) + expect($('.zen-toggle-comment').prop('checked')).toBe(false) + + describe 'on exit', -> + beforeEach -> + enterZen() + + it 'unpauses Mousetrap', -> + spyOn(Mousetrap, 'unpause') + exitZen() + expect(Mousetrap.unpause).toHaveBeenCalled() + + it 'restores the scroll position', -> + spyOn(@zen, 'restoreScroll') + exitZen() + expect(@zen.restoreScroll).toHaveBeenCalledWith(456) diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb new file mode 100644 index 00000000000..23f83339ec5 --- /dev/null +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require 'nokogiri' + +module Gitlab + describe Asciidoc do + + let(:input) { '<b>ascii</b>' } + let(:context) { {} } + let(:html) { 'H<sub>2</sub>O' } + + context "without project" do + + it "should convert the input using Asciidoctor and default options" do + expected_asciidoc_opts = { safe: :secure, backend: :html5, + attributes: described_class::DEFAULT_ADOC_ATTRS } + + expect(Asciidoctor).to receive(:convert) + .with(input, expected_asciidoc_opts).and_return(html) + + expect( render(input, context) ).to eql html + end + + context "with asciidoc_opts" do + + let(:asciidoc_opts) { {safe: :safe, attributes: ['foo']} } + + it "should merge the options with default ones" do + expected_asciidoc_opts = { safe: :safe, backend: :html5, + attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } + + expect(Asciidoctor).to receive(:convert) + .with(input, expected_asciidoc_opts).and_return(html) + + render(input, context, asciidoc_opts) + end + end + end + + context "with project in context" do + + let(:context) { {project: create(:project)} } + + it "should filter converted input via HTML pipeline and return result" do + filtered_html = '<b>ASCII</b>' + + allow(Asciidoctor).to receive(:convert).and_return(html) + expect_any_instance_of(HTML::Pipeline).to receive(:call) + .with(html, context) + .and_return(output: Nokogiri::HTML.fragment(filtered_html)) + + expect( render('foo', context) ).to eql filtered_html + end + end + + def render(*args) + described_class.render(*args) + end + end +end diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index d0aad54f677..42c9946d2a9 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -156,7 +156,7 @@ describe Grack::Auth do end expect(attempt_login(true)).to eq(200) - expect(Rack::Attack::Allow2Ban.send(:banned?, ip)).to eq(nil) + expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey for n in 0..maxretry do expect(attempt_login(false)).to eq(401) diff --git a/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb b/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb deleted file mode 100644 index 2ac496fd669..00000000000 --- a/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require "spec_helper" - -describe 'RackAttackHelpers' do - describe 'reset' do - let(:discriminator) { 'test-key'} - let(:maxretry) { 5 } - let(:period) { 1.minute } - let(:options) { { findtime: period, bantime: 60, maxretry: maxretry } } - - def do_filter - for i in 1..maxretry - 1 do - status = Rack::Attack::Allow2Ban.filter(discriminator, options) { true } - expect(status).to eq(false) - end - end - - def do_reset - Rack::Attack::Allow2Ban.reset(discriminator, options) - end - - before do - do_reset - end - - after do - do_reset - end - - it 'user is not banned after n - 1 retries' do - do_filter - do_reset - do_filter - end - end -end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index cb7b0fbb890..5d7ff4f6122 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -1,131 +1,131 @@ require 'spec_helper' describe Gitlab::ClosingIssueExtractor do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:iid1) { issue.iid } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:reference) { issue.to_reference } subject { described_class.new(project, project.creator) } describe "#closed_by_message" do context 'with a single reference' do it do - message = "Awesome commit (Closes ##{iid1})" + message = "Awesome commit (Closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (closes ##{iid1})" + message = "Awesome commit (closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Closed ##{iid1}" + message = "Closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "closed ##{iid1}" + message = "closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Closing ##{iid1}" + message = "Closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "closing ##{iid1}" + message = "closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Close ##{iid1}" + message = "Close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "close ##{iid1}" + message = "close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (Fixes ##{iid1})" + message = "Awesome commit (Fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (fixes ##{iid1})" + message = "Awesome commit (fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fixed ##{iid1}" + message = "Fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fixed ##{iid1}" + message = "fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fixing ##{iid1}" + message = "Fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fixing ##{iid1}" + message = "fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fix ##{iid1}" + message = "Fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fix ##{iid1}" + message = "fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (Resolves ##{iid1})" + message = "Awesome commit (Resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (resolves ##{iid1})" + message = "Awesome commit (resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolved ##{iid1}" + message = "Resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolved ##{iid1}" + message = "resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolving ##{iid1}" + message = "Resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolving ##{iid1}" + message = "resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolve ##{iid1}" + message = "Resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolve ##{iid1}" + message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end end @@ -133,40 +133,40 @@ describe Gitlab::ClosingIssueExtractor do context 'with multiple references' do let(:other_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) } - let(:iid2) { other_issue.iid } - let(:iid3) { third_issue.iid } + let(:reference2) { other_issue.to_reference } + let(:reference3) { third_issue.to_reference } it 'fetches issues in single line message' do - message = "Closes ##{iid1} and fix ##{iid2}" + message = "Closes #{reference} and fix #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do - message = "Closes ##{iid1}, closes ##{iid2}" + message = "Closes #{reference}, closes #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do - message = "Closes ##{iid1}, ##{iid2} and ##{iid3}" + message = "Closes #{reference}, #{reference2} and #{reference3}" expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do - message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}" + message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches issues in hybrid message' do - message = "Awesome commit (closes ##{iid1})\n"\ - "Also fixing issues ##{iid2}, ##{iid3} and #4" + message = "Awesome commit (closes #{reference})\n"\ + "Also fixing issues #{reference2}, #{reference3} and #4" expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 39be9d64644..c7291689e32 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -115,18 +115,10 @@ describe Gitlab::GitAccess do let(:actor) { key } context 'pull code' do - context 'allowed' do - before { key.projects << project } - subject { access.download_access_check } - - it { expect(subject.allowed?).to be_truthy } - end - - context 'denied' do - subject { access.download_access_check } + before { key.projects << project } + subject { access.download_access_check } - it { expect(subject.allowed?).to be_falsey } - end + it { expect(subject.allowed?).to be_truthy } end end end diff --git a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb b/spec/lib/gitlab/gitlab_markdown_helper_spec.rb deleted file mode 100644 index ab613193f41..00000000000 --- a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Gitlab::MarkdownHelper do - describe '#markup?' do - %w(textile rdoc org creole wiki - mediawiki rst adoc asciidoc asc).each do |type| - it "returns true for #{type} files" do - expect(Gitlab::MarkdownHelper.markup?("README.#{type}")).to be_truthy - end - end - - it 'returns false when given a non-markup filename' do - expect(Gitlab::MarkdownHelper.markup?('README.rb')).not_to be_truthy - end - end - - describe '#gitlab_markdown?' do - %w(mdown md markdown).each do |type| - it "returns true for #{type} files" do - expect(Gitlab::MarkdownHelper.gitlab_markdown?("README.#{type}")).to be_truthy - end - end - - it 'returns false when given a non-markdown filename' do - expect(Gitlab::MarkdownHelper.gitlab_markdown?('README.rb')).not_to be_truthy - end - end -end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 707a0521ab3..2189e313d6a 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::LDAP::Access do context 'when the user is found' do before { Gitlab::LDAP::Person.stub(find_by_dn: :ldap_user) } - context 'and the user is diabled via active directory' do + context 'and the user is disabled via active directory' do before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: true) } it { is_expected.to be_falsey } @@ -36,9 +36,28 @@ describe Gitlab::LDAP::Access do it { is_expected.to be_truthy } - it "should unblock user in GitLab" do - access.allowed? - user.should_not be_blocked + context 'when auto-created users are blocked' do + + before do + Gitlab::LDAP::Config.any_instance.stub(block_auto_created_users: true) + end + + it "does not unblock user in GitLab" do + access.allowed? + user.should be_blocked + end + end + + context "when auto-created users are not blocked" do + + before do + Gitlab::LDAP::Config.any_instance.stub(block_auto_created_users: false) + end + + it "should unblock user in GitLab" do + access.allowed? + user.should_not be_blocked + end end end diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb index 0bbdc11a979..a14cb2da089 100644 --- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb +++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb @@ -2,11 +2,9 @@ require 'spec_helper' module Gitlab::Markdown describe AutolinkFilter do - let(:link) { 'http://about.gitlab.com/' } + include FilterSpecHelper - def filter(html, options = {}) - described_class.call(html, options) - end + let(:link) { 'http://about.gitlab.com/' } it 'does nothing when :autolink is false' do exp = act = link diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 7274cb309a0..e8391cc7aca 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -2,40 +2,42 @@ require 'spec_helper' module Gitlab::Markdown describe CommitRangeReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit1) { project.commit } let(:commit2) { project.commit("HEAD~2") } + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + it 'requires project context' do - expect { described_class.call('Commit Range 1c002d..d200c1', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>" + exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "#{commit1.id}...#{commit2.id}" } - let(:reference2) { "#{commit1.id}..#{commit2.id}" } + let(:reference) { range.to_reference } + let(:reference2) { range2.to_reference } it 'links to a valid two-dot reference' do doc = filter("See #{reference2}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id) + to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) end it 'links to a valid three-dot reference' do doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id) + to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) end it 'links to a valid short ID' do @@ -51,7 +53,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("See (#{reference}.)") - exp = Regexp.escape("#{commit1.short_id}...#{commit2.short_id}") + exp = Regexp.escape(range.to_s) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end @@ -65,7 +67,7 @@ module Gitlab::Markdown it 'includes a title attribute' do doc = filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}" + expect(doc.css('a').first.attr('title')).to eq range.reference_title end it 'includes default classes' do @@ -95,9 +97,11 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } - let(:commit1) { project.commit } - let(:commit2) { project.commit("HEAD~2") } - let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } + let(:reference) { range.to_reference(project) } + + before do + range.project = project2 + end context 'when user can access reference' do before { allow_cross_reference! } @@ -106,21 +110,21 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id) + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}" + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" expect(filter(act).to_html).to eq exp - exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index cc32a4fcf03..a10d43c9a02 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -2,14 +2,13 @@ require 'spec_helper' module Gitlab::Markdown describe CommitReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit) { project.commit } it 'requires project context' do - expect { described_class.call('Commit 1c002d', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -47,10 +46,11 @@ module Gitlab::Markdown end it 'ignores invalid commit IDs' do - exp = act = "See #{reference.reverse}" + invalid = invalidate_reference(reference) + exp = act = "See #{invalid}" expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(reference.reverse) + expect(project.repository).to receive(:commit).with(invalid) expect(filter(act).to_html).to eq exp end @@ -93,8 +93,8 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } - let(:commit) { project.commit } - let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" } + let(:commit) { project2.commit } + let(:reference) { commit.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -109,12 +109,12 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape(project2.path_with_namespace) + exp = Regexp.escape(project2.to_reference) expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" + exp = act = "Committed #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb index 18d55c4818f..11efd9bb4cd 100644 --- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb +++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe EmojiFilter do - def filter(html, contexts = {}) - described_class.call(html, contexts) - end + include FilterSpecHelper before do ActionController::Base.asset_host = 'https://foo.com' diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb index b19bc125b92..f16095bc2b2 100644 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -2,26 +2,25 @@ require 'spec_helper' module Gitlab::Markdown describe ExternalIssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper end let(:project) { create(:jira_project) } - let(:issue) { double('issue', iid: 123) } context 'JIRA issue references' do - let(:reference) { "JIRA-#{issue.iid}" } + let(:issue) { ExternalIssue.new('JIRA-123', project) } + let(:reference) { issue.to_reference } it 'requires project context' do - expect { described_class.call('Issue JIRA-123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>" + exp = act = "<#{elem}>Issue #{reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end @@ -33,13 +32,6 @@ module Gitlab::Markdown expect(filter(act).to_html).to eq exp end - %w(pre code a style).each do |elem| - it "ignores references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end - it 'links to a valid reference' do doc = filter("Issue #{reference}") expect(doc.css('a').first.attr('href')) diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb new file mode 100644 index 00000000000..a040b34577b --- /dev/null +++ b/spec/lib/gitlab/markdown/external_link_filter_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe ExternalLinkFilter do + include FilterSpecHelper + + it 'ignores elements without an href attribute' do + exp = act = %q(<a id="ignored">Ignore Me</a>) + expect(filter(act).to_html).to eq exp + end + + it 'ignores non-HTTP(S) links' do + exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>) + expect(filter(act).to_html).to eq exp + end + + it 'skips internal links' do + internal = Gitlab.config.gitlab.url + exp = act = %Q(<a href="#{internal}/sign_in">Login</a>) + expect(filter(act).to_html).to eq exp + end + + it 'adds rel="nofollow" to external links' do + act = %q(<a href="https://google.com/">Google</a>) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to eq 'nofollow' + end + end +end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 08382b3e7e8..fa43d33794d 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe IssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper @@ -12,24 +12,23 @@ module Gitlab::Markdown let(:issue) { create(:issue, project: project) } it 'requires project context' do - expect { described_class.call('Issue #123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>" + exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "##{issue.iid}" } + let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) - exp = act = "Issue ##{issue.iid}" + exp = act = "Issue #{reference}" expect(filter(act).to_html).to eq exp end @@ -46,9 +45,9 @@ module Gitlab::Markdown end it 'ignores invalid issue IDs' do - exp = act = "Fixed ##{issue.iid + 1}" + invalid = invalidate_reference(reference) + exp = act = "Fixed #{invalid}" - expect(project).to receive(:get_issue).with(issue.iid + 1).and_return(nil) expect(filter(act).to_html).to eq exp end @@ -92,7 +91,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + let(:reference) { issue.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -101,7 +100,7 @@ module Gitlab::Markdown expect_any_instance_of(Project).to receive(:get_issue). with(issue.iid).and_return(nil) - exp = act = "Issue ##{issue.iid}" + exp = act = "Issue #{reference}" expect(filter(act).to_html).to eq exp end @@ -118,7 +117,7 @@ module Gitlab::Markdown end it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}" + exp = act = "Fixed #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index 9f898837466..cf3337b1ba1 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -3,15 +3,14 @@ require 'html/pipeline' module Gitlab::Markdown describe LabelReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:label) { create(:label, project: project) } - let(:reference) { "~#{label.id}" } + let(:reference) { label.to_reference } it 'requires project context' do - expect { described_class.call('Label ~123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -36,7 +35,7 @@ module Gitlab::Markdown link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true) + expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end it 'adds to the results hash' do @@ -70,7 +69,7 @@ module Gitlab::Markdown end it 'ignores invalid label IDs' do - exp = act = "Label ~#{label.id + 1}" + exp = act = "Label #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -78,7 +77,7 @@ module Gitlab::Markdown context 'String-based single-word references' do let(:label) { create(:label, name: 'gfm', project: project) } - let(:reference) { "~#{label.name}" } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } it 'links to a valid reference' do doc = filter("See #{reference}") @@ -94,60 +93,41 @@ module Gitlab::Markdown end it 'ignores invalid label names' do - exp = act = "Label ~#{label.name.reverse}" + exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" expect(filter(act).to_html).to eq exp end end context 'String-based multi-word references in quotes' do - let(:label) { create(:label, name: 'gfm references', project: project) } + let(:label) { create(:label, name: 'gfm references', project: project) } + let(:reference) { label.to_reference(:name) } - context 'in single quotes' do - let(:reference) { "~'#{label.name}'" } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end - - it 'ignores invalid label names' do - exp = act = "Label ~'#{label.name.reverse}'" + it 'links to a valid reference' do + doc = filter("See #{reference}") - expect(filter(act).to_html).to eq exp - end + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' end - context 'in double quotes' do - let(:reference) { %(~"#{label.name}") } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end - it 'ignores invalid label names' do - exp = act = %(Label ~"#{label.name.reverse}") + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - expect(filter(act).to_html).to eq exp - end + expect(filter(act).to_html).to eq exp end end + + describe 'edge cases' do + it 'gracefully handles non-references matching the pattern' do + exp = act = '(format nil "~0f" 3.0) ; 3.0' + expect(filter(act).to_html).to eq exp + end + end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index d6e745114f2..5945302a2da 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -2,25 +2,24 @@ require 'spec_helper' module Gitlab::Markdown describe MergeRequestReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do - expect { described_class.call('MergeRequest !123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>" + exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "!#{merge.iid}" } + let(:reference) { merge.to_reference } it 'links to a valid reference' do doc = filter("See #{reference}") @@ -35,7 +34,7 @@ module Gitlab::Markdown end it 'ignores invalid merge IDs' do - exp = act = "Merge !#{merge.iid + 1}" + exp = act = "Merge #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -80,7 +79,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } let(:merge) { create(:merge_request, source_project: project2) } - let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + let(:reference) { merge.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -99,7 +98,7 @@ module Gitlab::Markdown end it 'ignores invalid merge IDs on the referenced project' do - exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}" + exp = act = "Merge #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb new file mode 100644 index 00000000000..5ee5310825d --- /dev/null +++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe RelativeLinkFilter do + def filter(doc) + described_class.call(doc, { + commit: project.commit, + project: project, + project_wiki: project_wiki, + ref: ref, + requested_path: requested_path + }) + end + + def image(path) + %(<img src="#{path}" />) + end + + def link(path) + %(<a href="#{path}">#{path}</a>) + end + + let(:project) { create(:project) } + let(:project_path) { project.path_with_namespace } + let(:ref) { 'markdown' } + let(:project_wiki) { nil } + let(:requested_path) { '/' } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + shared_examples :relative_to_requested do + it 'rebuilds URL relative to the requested path' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" + end + end + + context 'with a project_wiki' do + let(:project_wiki) { double('ProjectWiki') } + include_examples :preserve_unchanged + end + + context 'without a repository' do + let(:project) { create(:empty_project) } + include_examples :preserve_unchanged + end + + context 'with an empty repository' do + let(:project) { create(:project_empty_repo) } + include_examples :preserve_unchanged + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a file in the repo' do + doc = filter(link('doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo with an anchor' do + doc = filter(link('README.md#section')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/README.md#section" + end + + it 'rebuilds relative URL for a directory in the repo' do + doc = filter(link('doc/api/')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/tree/#{ref}/doc/api" + end + + it 'rebuilds relative URL for an image in the repo' do + doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'does not modify relative URL with an anchor only' do + doc = filter(link('#section-1')) + expect(doc.at_css('a')['href']).to eq '#section-1' + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + context 'when requested path is a file in the repo' do + let(:requested_path) { 'doc/api/README.md' } + include_examples :relative_to_requested + end + + context 'when requested path is a directory in the repo' do + let(:requested_path) { 'doc/api' } + include_examples :relative_to_requested + end + end + end +end diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb index ab909a68635..e50c82d0b3c 100644 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe SanitizationFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper describe 'default whitelist' do it 'sanitizes tags that are not whitelisted' do @@ -29,19 +27,36 @@ module Gitlab::Markdown exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" expect(filter(act).to_html).to eq exp end + + it 'sanitizes `class` attribute on any element' do + act = %q{<strong class="foo">Strong</strong>} + expect(filter(act).to_html).to eq %q{<strong>Strong</strong>} + end + + it 'sanitizes `id` attribute on any element' do + act = %q{<em id="foo">Emphasis</em>} + expect(filter(act).to_html).to eq %q{<em>Emphasis</em>} + end end describe 'custom whitelist' do - it 'allows `class` attribute on any element' do - exp = act = %q{<strong class="foo">Strong</strong>} - expect(filter(act).to_html).to eq exp + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 4 end - it 'allows `id` attribute on any element' do - exp = act = %q{<em id="foo">Emphasis</em>} + it 'allows syntax highlighting' do + exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} expect(filter(act).to_html).to eq exp end + it 'sanitizes `class` attribute from non-highlight spans' do + act = %q{<span class="k">def</span>} + expect(filter(act).to_html).to eq %q{<span>def</span>} + end + it 'allows `style` attribute on table elements' do html = <<-HTML.strip_heredoc <table> @@ -77,5 +92,27 @@ module Gitlab::Markdown expect(doc.at_css('a')['href']).to be_nil end end + + context 'when pipeline is :description' do + it 'uses a stricter whitelist' do + doc = filter('<h1>Description</h1>', pipeline: :description) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html).to eq exp + end + end + end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index a4b331157af..38619a3c07f 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -2,15 +2,14 @@ require 'spec_helper' module Gitlab::Markdown describe SnippetReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:snippet) { create(:project_snippet, project: project) } - let(:reference) { "$#{snippet.id}" } + let(:reference) { snippet.to_reference } it 'requires project context' do - expect { described_class.call('Snippet $123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -34,7 +33,7 @@ module Gitlab::Markdown end it 'ignores invalid snippet IDs' do - exp = act = "Snippet $#{snippet.id + 1}" + exp = act = "Snippet #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -79,7 +78,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, namespace: namespace) } let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + let(:reference) { snippet.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -97,7 +96,7 @@ module Gitlab::Markdown end it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}" + exp = act = "See #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb index f383a5850d5..ddf583a72c1 100644 --- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb +++ b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb @@ -4,9 +4,7 @@ require 'spec_helper' module Gitlab::Markdown describe TableOfContentsFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper def header(level, text) "<h#{level}>#{text}</h#{level}>\n" diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb new file mode 100644 index 00000000000..94f39cc966e --- /dev/null +++ b/spec/lib/gitlab/markdown/task_list_filter_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe TaskListFilter do + include FilterSpecHelper + + it 'does not apply `task-list` class to non-task lists' do + exp = act = %(<ul><li>Item</li></ul>) + expect(filter(act).to_html).to eq exp + end + end +end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index 922502ada33..08e6941028f 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -2,67 +2,65 @@ require 'spec_helper' module Gitlab::Markdown describe UserReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper - let(:project) { create(:empty_project) } - let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:reference) { user.to_reference } it 'requires project context' do - expect { described_class.call('Example @mention', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end it 'ignores invalid users' do - exp = act = 'Hey @somebody' + exp = act = "Hey #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq(exp) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>" + exp = act = "<#{elem}>Hey #{reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'mentioning @all' do + let(:reference) { User.reference_prefix + 'all' } + before do project.team << [project.creator, :developer] end it 'supports a special @all mention' do - doc = filter("Hey @all") + doc = filter("Hey #{reference}") expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) end it 'adds to the results hash' do - result = pipeline_result('Hey @all') + result = pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq [project.creator] end end context 'mentioning a user' do - let(:reference) { "@#{user.username}" } - it 'links to a User' do doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) end - # TODO (rspeicher): This test might be overkill it 'links to a User with a period' do user = create(:user, name: 'alphA.Beta') - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end - # TODO (rspeicher): This test might be overkill it 'links to a User with an underscore' do user = create(:user, name: 'ping_pong_king') - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end @@ -73,10 +71,9 @@ module Gitlab::Markdown end context 'mentioning a group' do - let(:group) { create(:group) } - let(:user) { create(:user) } - - let(:reference) { "@#{group.name}" } + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:reference) { group.to_reference } context 'that the current user can read' do before do @@ -108,23 +105,23 @@ module Gitlab::Markdown end it 'links with adjacent text' do - skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.' - doc = filter("Mention me (@#{user.username}.)") - expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) + skip "TODO (rspeicher): Re-enable when usernames can't end in periods." + doc = filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) end it 'includes default classes' do - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' end it 'includes an optional custom class' do - doc = filter("Hey @#{user.username}", reference_class: 'custom') + doc = filter("Hey #{reference}", reference_class: 'custom') expect(doc.css('a').first.attr('class')).to include 'custom' end it 'supports an :only_path context' do - doc = filter("Hey @#{user.username}", only_path: true) + doc = filter("Hey #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb new file mode 100644 index 00000000000..7e716e866b1 --- /dev/null +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::MarkupHelper do + describe '#markup?' do + %w(textile rdoc org creole wiki + mediawiki rst adoc ad asciidoc mdown md markdown).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.markup?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-markup filename' do + expect(Gitlab::MarkupHelper.markup?('README.rb')).not_to be_truthy + end + end + + describe '#gitlab_markdown?' do + %w(mdown md markdown).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.gitlab_markdown?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-markdown filename' do + expect(Gitlab::MarkupHelper.gitlab_markdown?('README.rb')).not_to be_truthy + end + end + + describe '#asciidoc?' do + %w(adoc ad asciidoc ADOC).each do |type| + it "returns true for #{type} files" do + expect(Gitlab::MarkupHelper.asciidoc?("README.#{type}")).to be_truthy + end + end + + it 'returns false when given a non-asciidoc filename' do + expect(Gitlab::MarkupHelper.asciidoc?('README.rb')).not_to be_truthy + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 9801dc16554..f921dd9cc09 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -16,11 +16,35 @@ describe Gitlab::ReferenceExtractor do expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) end + it 'ignores user mentions inside specific elements' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + @u_offteam = create(:user, username: 'offteam') + + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] + + subject.analyze(%Q{ + Inline code: `@foo` + + Code block: + + ``` + @bar + ``` + + Quote: + + > @offteam + }) + expect(subject.users).to eq([]) + end + it 'accesses valid issue objects' do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) - subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.") + subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") expect(subject.issues).to eq([@i0, @i1]) end @@ -82,7 +106,7 @@ describe Gitlab::ReferenceExtractor do end it 'handles project issue references' do - subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}") + subject.analyze("this refers issue #{issue.to_reference(project)}") extracted = subject.issues expect(extracted.size).to eq(1) expect(extracted).to eq([issue]) diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index ce3ea6c260a..baa4bd0f28f 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -20,5 +20,20 @@ describe Gitlab::Upgrader do upgrader.stub(current_version_raw: "5.3.0") expect(upgrader.latest_version_raw).to eq("v5.4.2") end + + it 'should get the latest version from tags' do + allow(upgrader).to receive(:fetch_git_tags).and_return([ + '6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1', + 'facfec4b242ce151af224e20715d58e628aa5e74 refs/tags/v7.1.1^{}', + 'f7068d99c79cf79befbd388030c051bb4b5e86d4 refs/tags/v7.10.4', + '337225a4fcfa9674e2528cb6d41c46556bba9dfa refs/tags/v7.10.4^{}', + '880e0ba0adbed95d087f61a9a17515e518fc6440 refs/tags/v7.11.1', + '6584346b604f981f00af8011cd95472b2776d912 refs/tags/v7.11.1^{}', + '43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2', + 'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}', + '0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4', + 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}']) + expect(upgrader.latest_version_raw).to eq("v7.11.2") + end end end diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb index af399f3a731..37240d51310 100644 --- a/spec/lib/repository_cache_spec.rb +++ b/spec/lib/repository_cache_spec.rb @@ -1,4 +1,3 @@ -require 'rspec' require_relative '../../lib/repository_cache' describe RepositoryCache do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index dbcf7286e45..c40ae7b5703 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -5,6 +5,8 @@ describe Notify do include EmailSpec::Matchers include RepoHelpers + new_user_address = 'newguy@example.com' + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } let(:gitlab_sender) { Gitlab.config.gitlab.email_from } let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } @@ -55,18 +57,9 @@ describe Notify do end end - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: 'newguy@example.com', created_by_id: 1) } - - token = 'kETLwRaayvigPq_x3SNM' - - subject { Notify.new_user_email(new_user.id, token) } - - it_behaves_like 'an email sent from GitLab' - + shared_examples 'a new user email' do |user_email, site_path| it 'is sent to the new user' do - is_expected.to deliver_to new_user.email + is_expected.to deliver_to user_email end it 'has the correct subject' do @@ -74,9 +67,25 @@ describe Notify do end it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{new_user.email}/ + is_expected.to have_body_text /#{user_email}/ end + it 'includes a link to the site' do + is_expected.to have_body_text /#{site_path}/ + end + end + + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + + token = 'kETLwRaayvigPq_x3SNM' + + subject { Notify.new_user_email(new_user.id, token) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email', new_user_address + it 'contains the password text' do is_expected.to have_body_text /Click here to set your password/ end @@ -88,39 +97,26 @@ describe Notify do ) end - it 'includes a link to the site' do - is_expected.to have_body_text /#{example_site_path}/ + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) end end describe 'for users that signed up, the email' do let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: 'newguy@example.com', password: "securePassword") } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } subject { Notify.new_user_email(new_user.id) } it_behaves_like 'an email sent from GitLab' - - it 'is sent to the new user' do - is_expected.to deliver_to new_user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{new_user.email}/ - end + it_behaves_like 'a new user email', new_user_address it 'should not contain the new user\'s password' do is_expected.not_to have_body_text /password/ end - - it 'includes a link to the site' do - is_expected.to have_body_text /#{example_site_path}/ - end end describe 'user added ssh key' do @@ -189,7 +185,7 @@ describe Notify do context 'for issues' do let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) } - let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: Faker::Lorem.sentence) } + let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: FFaker::Lorem.sentence) } describe 'that are new' do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } @@ -277,7 +273,7 @@ describe Notify do context 'for merge requests' do let(:merge_author) { create(:user) } let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) } - let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: Faker::Lorem.sentence) } + let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: FFaker::Lorem.sentence) } describe 'that are new' do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 31ee3e99cad..e7fb43ff335 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -1,6 +1,12 @@ require 'spec_helper' describe CommitRange do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + let(:sha_from) { 'f3f85602' } let(:sha_to) { 'e86e1013' } @@ -21,6 +27,23 @@ describe CommitRange do end end + describe '#to_reference' do + let(:project) { double('project', to_reference: 'namespace1/project') } + + before do + range.project = project + end + + it 'returns a String reference to the object' do + expect(range.to_reference).to eq range.to_s + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}" + end + end + describe '#reference_title' do it 'returns the correct String for three-dot ranges' do expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ad2ac143d97..27eb02a870b 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,8 +1,28 @@ require 'spec_helper' describe Commit do - let(:project) { create :project } - let(:commit) { project.commit } + let(:project) { create(:project) } + let(:commit) { project.commit } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Mentionable) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(StaticModel) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(commit.to_reference).to eq commit.id + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + end + end describe '#title' do it "returns no_commit_message when safe_message is blank" do diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb new file mode 100644 index 00000000000..7744610db78 --- /dev/null +++ b/spec/models/external_issue_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe ExternalIssue do + let(:project) { double('project', to_reference: 'namespace1/project1') } + let(:issue) { described_class.new('EXT-1234', project) } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(issue.to_reference).to eq issue.id + end + end + + describe '#title' do + it 'returns a title' do + expect(issue.title).to eq "External Issue #{issue}" + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9428224a64f..80638fc8db2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -18,16 +18,30 @@ require 'spec_helper' describe Group do let!(:group) { create(:group) } - describe "Associations" do + describe 'associations' do it { is_expected.to have_many :projects } it { is_expected.to have_many :group_members } end - it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } - it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } - it { is_expected.not_to validate_presence_of :owner } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.not_to validate_presence_of :owner } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(group.to_reference).to eq "@#{group.name}" + end + end describe :users do it { expect(group.users).to eq(group.owners) } diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 4e0d50d7f3f..dae7e399cfb 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 96bf74d45da..fb5111dd9f5 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # require "spec_helper" @@ -21,4 +22,37 @@ describe ServiceHook do describe "Associations" do it { is_expected.to belong_to :service } end + + describe "execute" do + before(:each) do + @service_hook = create(:service_hook) + @data = { project_id: 1, data: {}} + + WebMock.stub_request(:post, @service_hook.url) + end + + it "POSTs to the web hook URL" do + @service_hook.execute(@data) + expect(WebMock).to have_requested(:post, @service_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook'} + ).once + end + + it "POSTs the data as JSON" do + json = @data.to_json + + @service_hook.execute(@data) + expect(WebMock).to have_requested(:post, @service_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook'} + ).once + end + + it "catches exceptions" do + expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") + + expect { + @service_hook.execute(@data) + }.to raise_error + end + end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 810b311a40b..edb21fc2e47 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # require "spec_helper" @@ -26,32 +27,47 @@ describe SystemHook do it "project_create hook" do Projects::CreateService.new(create(:user), name: 'empty').execute - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /project_create/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /project_create/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it "project_destroy hook" do user = create(:user) project = create(:empty_project, namespace: user.namespace) Projects::DestroyService.new(project, user, {}).execute - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /project_destroy/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /project_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it "user_create hook" do create(:user) - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_create/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_create/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it "user_destroy hook" do user = create(:user) user.destroy - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_destroy/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it "project_create hook" do user = create(:user) project = create(:project) project.team << [user, :master] - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_add_to_team/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it "project_destroy hook" do @@ -59,13 +75,17 @@ describe SystemHook do project = create(:project) project.team << [user, :master] project.project_members.destroy_all - expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with( + body: /user_remove_from_team/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} + ).once end it 'group create hook' do create(:group) expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /group_create/ + body: /group_create/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} ).once end @@ -73,7 +93,8 @@ describe SystemHook do group = create(:group) group.destroy expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /group_destroy/ + body: /group_destroy/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} ).once end @@ -82,7 +103,8 @@ describe SystemHook do user = create(:user) group.add_user(user, Gitlab::Access::MASTER) expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_add_to_group/ + body: /user_add_to_group/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} ).once end @@ -92,7 +114,8 @@ describe SystemHook do group.add_user(user, Gitlab::Access::MASTER) group.group_members.destroy_all expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_remove_from_group/ + body: /user_remove_from_group/, + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook'} ).once end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 67ec9193ad7..4c3f0cbcbbf 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -13,6 +13,7 @@ # issues_events :boolean default(FALSE), not null # merge_requests_events :boolean default(FALSE), not null # tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null # require 'spec_helper' @@ -52,22 +53,26 @@ describe ProjectHook do end it "POSTs to the web hook URL" do - @project_hook.execute(@data) - expect(WebMock).to have_requested(:post, @project_hook.url).once + @project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, @project_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook'} + ).once end it "POSTs the data as JSON" do json = @data.to_json - @project_hook.execute(@data) - expect(WebMock).to have_requested(:post, @project_hook.url).with(body: json).once + @project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, @project_hook.url).with( + headers: {'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook'} + ).once end it "catches exceptions" do expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") expect { - @project_hook.execute(@data) + @project_hook.execute(@data, 'push_hooks') }.to raise_error end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 20d823b40e5..614b648bb52 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -24,15 +24,30 @@ describe Issue do it { is_expected.to belong_to(:milestone) } end - describe "Mass assignment" do - end - describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(InternalId) } it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end subject { create(:issue) } + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "##{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)). + to eq "#{subject.project.to_reference}##{subject.iid}" + end + end + describe '#is_being_reassigned?' do it 'returns true if the issue assignee has changed' do subject.assignee = create(:user) @@ -45,11 +60,8 @@ describe Issue do describe '#is_being_reassigned?' do it 'returns issues assigned to user' do - user = create :user - - 2.times do - issue = create :issue, assignee: user - end + user = create(:user) + create_list(:issue, 2, assignee: user) expect(Issue.open_for(user).count).to eq 2 end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 8644ac46605..6518213d71c 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -14,30 +14,63 @@ require 'spec_helper' describe Label do let(:label) { create(:label) } - it { expect(label).to be_valid } - it { is_expected.to belong_to(:project) } + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:label_links).dependent(:destroy) } + it { is_expected.to have_many(:issues).through(:label_links).source(:target) } + end + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:project) } - describe 'Validation' do it 'should validate color code' do - expect(build(:label, color: 'G-ITLAB')).not_to be_valid - expect(build(:label, color: 'AABBCC')).not_to be_valid - expect(build(:label, color: '#AABBCCEE')).not_to be_valid - expect(build(:label, color: '#GGHHII')).not_to be_valid - expect(build(:label, color: '#')).not_to be_valid - expect(build(:label, color: '')).not_to be_valid - - expect(build(:label, color: '#AABBCC')).to be_valid + expect(label).not_to allow_value('G-ITLAB').for(:color) + expect(label).not_to allow_value('AABBCC').for(:color) + expect(label).not_to allow_value('#AABBCCEE').for(:color) + expect(label).not_to allow_value('GGHHII').for(:color) + expect(label).not_to allow_value('#').for(:color) + expect(label).not_to allow_value('').for(:color) + + expect(label).to allow_value('#AABBCC').for(:color) + expect(label).to allow_value('#abcdef').for(:color) end it 'should validate title' do - expect(build(:label, title: 'G,ITLAB')).not_to be_valid - expect(build(:label, title: 'G?ITLAB')).not_to be_valid - expect(build(:label, title: 'G&ITLAB')).not_to be_valid - expect(build(:label, title: '')).not_to be_valid + expect(label).not_to allow_value('G,ITLAB').for(:title) + expect(label).not_to allow_value('G?ITLAB').for(:title) + expect(label).not_to allow_value('G&ITLAB').for(:title) + expect(label).not_to allow_value('').for(:title) + + expect(label).to allow_value('GITLAB').for(:title) + expect(label).to allow_value('gitlab').for(:title) + expect(label).to allow_value("customer's request").for(:title) + end + end + + describe '#to_reference' do + context 'using id' do + it 'returns a String reference to the object' do + expect(label.to_reference).to eq "~#{label.id}" + expect(label.to_reference(double('project'))).to eq "~#{label.id}" + end + end + + context 'using name' do + it 'returns a String reference to the object' do + expect(label.to_reference(:name)).to eq %(~"#{label.name}") + end - expect(build(:label, title: 'GITLAB')).to be_valid - expect(build(:label, title: 'gitlab')).to be_valid + it 'uses id when name contains double quote' do + label = create(:label, name: %q{"irony"}) + expect(label.to_reference(:name)).to eq "~#{label.id}" + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 97b8abc49dd..0465aa34843 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -24,22 +24,45 @@ require 'spec_helper' describe MergeRequest do - describe "Validation" do - it { is_expected.to validate_presence_of(:target_branch) } - it { is_expected.to validate_presence_of(:source_branch) } + subject { create(:merge_request) } + + describe 'associations' do + it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } + it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } + + it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } end - describe "Mass assignment" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(InternalId) } + it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end - describe "Respond to" do + describe 'validation' do + it { is_expected.to validate_presence_of(:target_branch) } + it { is_expected.to validate_presence_of(:source_branch) } + end + + describe 'respond to' do it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) } end - describe 'modules' do - it { is_expected.to include_module(Issuable) } + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "!#{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}" + end end describe "#mr_and_commit_notes" do @@ -57,8 +80,6 @@ describe MergeRequest do end end - subject { create(:merge_request) } - describe '#is_being_reassigned?' do it 'returns true if the merge_request assignee has changed' do subject.assignee = create(:user) @@ -108,7 +129,7 @@ describe MergeRequest do it 'detects issues mentioned in the description' do issue2 = create(:issue, project: subject.project) - subject.description = "Closes ##{issue2.iid}" + subject.description = "Closes #{issue2.to_reference}" subject.project.stub(default_branch: subject.target_branch) expect(subject.closes_issues).to include(issue2) diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 45171e1bf64..eb73aa763fc 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -47,7 +47,7 @@ describe Milestone do it "should recover from dividing by zero" do expect(milestone.issues).to receive(:count).and_return(0) - expect(milestone.percent_complete).to eq(100) + expect(milestone.percent_complete).to eq(0) end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 4a6bfdb2910..ddacba58261 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -20,68 +20,88 @@ require 'spec_helper' describe Note do - describe "Associations" do + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:noteable) } it { is_expected.to belong_to(:author).class_name('User') } end - describe "Mass assignment" do - end - - describe "Validation" do + describe 'validation' do it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:project) } end - describe "Voting score" do - let(:project) { create(:project) } + describe '#votable?' do + it 'is true for issue notes' do + note = build(:note_on_issue) + expect(note).to be_votable + end + + it 'is true for merge request notes' do + note = build(:note_on_merge_request) + expect(note).to be_votable + end + + it 'is false for merge request diff notes' do + note = build(:note_on_merge_request_diff) + expect(note).not_to be_votable + end + + it 'is false for commit notes' do + note = build(:note_on_commit) + expect(note).not_to be_votable + end - it "recognizes a neutral note" do - note = create(:votable_note, note: "This is not a +1 note") + it 'is false for commit diff notes' do + note = build(:note_on_commit_diff) + expect(note).not_to be_votable + end + end + + describe 'voting score' do + it 'recognizes a neutral note' do + note = build(:votable_note, note: 'This is not a +1 note') expect(note).not_to be_upvote expect(note).not_to be_downvote end - it "recognizes a neutral emoji note" do + it 'recognizes a neutral emoji note' do note = build(:votable_note, note: "I would :+1: this, but I don't want to") expect(note).not_to be_upvote expect(note).not_to be_downvote end - it "recognizes a +1 note" do - note = create(:votable_note, note: "+1 for this") + it 'recognizes a +1 note' do + note = build(:votable_note, note: '+1 for this') expect(note).to be_upvote end - it "recognizes a +1 emoji as a vote" do - note = build(:votable_note, note: ":+1: for this") + it 'recognizes a +1 emoji as a vote' do + note = build(:votable_note, note: ':+1: for this') expect(note).to be_upvote end - it "recognizes a thumbsup emoji as a vote" do - note = build(:votable_note, note: ":thumbsup: for this") + it 'recognizes a thumbsup emoji as a vote' do + note = build(:votable_note, note: ':thumbsup: for this') expect(note).to be_upvote end - it "recognizes a -1 note" do - note = create(:votable_note, note: "-1 for this") + it 'recognizes a -1 note' do + note = build(:votable_note, note: '-1 for this') expect(note).to be_downvote end - it "recognizes a -1 emoji as a vote" do - note = build(:votable_note, note: ":-1: for this") + it 'recognizes a -1 emoji as a vote' do + note = build(:votable_note, note: ':-1: for this') expect(note).to be_downvote end - it "recognizes a thumbsdown emoji as a vote" do - note = build(:votable_note, note: ":thumbsdown: for this") + it 'recognizes a thumbsdown emoji as a vote' do + note = build(:votable_note, note: ':thumbsdown: for this') expect(note).to be_downvote end end - let(:project) { create(:project) } - describe "Commit notes" do let!(:note) { create(:note_on_commit, note: "+1 from me") } let!(:commit) { note.noteable } @@ -100,10 +120,6 @@ describe Note do it "should be recognized by #for_commit?" do expect(note).to be_for_commit end - - it "should not be votable" do - expect(note).not_to be_votable - end end describe "Commit diff line notes" do @@ -128,461 +144,7 @@ describe Note do end end - describe "Issue notes" do - let!(:note) { create(:note_on_issue, note: "+1 from me") } - - it "should not be votable" do - expect(note).to be_votable - end - end - - describe "Merge request notes" do - let!(:note) { create(:note_on_merge_request, note: "+1 from me") } - - it "should be votable" do - expect(note).to be_votable - end - end - - describe "Merge request diff line notes" do - let!(:note) { create(:note_on_merge_request_diff, note: "+1 from me") } - - it "should not be votable" do - expect(note).not_to be_votable - end - end - - describe '#create_status_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:status) { 'new_status' } - - subject { Note.create_status_change_note(thing, project, author, status, nil) } - - it 'creates and saves a Note' do - is_expected.to be_a Note - expect(subject.id).not_to be_nil - end - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(thing) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(thing.project) } - end - - describe '#author' do - subject { super().author } - it { is_expected.to eq(author) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("Status changed to #{status}") } - end - - it 'appends a back-reference if a closing mentionable is supplied' do - commit = double('commit', gfm_reference: 'commit 123456') - n = Note.create_status_change_note(thing, project, author, status, commit) - - expect(n.note).to eq("Status changed to #{status} by commit 123456") - end - end - - describe '#create_assignee_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:assignee) { create(:user, username: "assigned_user") } - - subject { Note.create_assignee_change_note(thing, project, author, assignee) } - - context 'creates and saves a Note' do - it { is_expected.to be_a Note } - - describe '#id' do - subject { super().id } - it { is_expected.not_to be_nil } - end - end - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(thing) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(thing.project) } - end - - describe '#author' do - subject { super().author } - it { is_expected.to eq(author) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq('Reassigned to @assigned_user') } - end - - context 'assignee is removed' do - let(:assignee) { nil } - - describe '#note' do - subject { super().note } - it { is_expected.to eq('Assignee removed') } - end - end - end - - describe '#create_labels_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:label1) { create(:label) } - let(:label2) { create(:label) } - let(:added_labels) { [label1, label2] } - let(:removed_labels) { [] } - - subject { Note.create_labels_change_note(thing, project, author, added_labels, removed_labels) } - - context 'creates and saves a Note' do - it { is_expected.to be_a Note } - - describe '#id' do - subject { super().id } - it { is_expected.not_to be_nil } - end - end - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(thing) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(thing.project) } - end - - describe '#author' do - subject { super().author } - it { is_expected.to eq(author) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("Added ~#{label1.id} ~#{label2.id} labels") } - end - - context 'label is removed' do - let(:added_labels) { [label1] } - let(:removed_labels) { [label2] } - - describe '#note' do - subject { super().note } - it { is_expected.to eq("Added ~#{label1.id} and removed ~#{label2.id} labels") } - end - end - end - - describe '#create_milestone_change_note' do - let(:project) { create(:project) } - let(:thing) { create(:issue, project: project) } - let(:milestone) { create(:milestone, project: project, title: "first_milestone") } - let(:author) { create(:user) } - - subject { Note.create_milestone_change_note(thing, project, author, milestone) } - - context 'creates and saves a Note' do - it { is_expected.to be_a Note } - - describe '#id' do - subject { super().id } - it { is_expected.not_to be_nil } - end - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(thing.project) } - end - - describe '#author' do - subject { super().author } - it { is_expected.to eq(author) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("Milestone changed to first_milestone") } - end - end - - describe '#create_cross_reference_note' do - let(:project) { create(:project) } - let(:author) { create(:user) } - let(:issue) { create(:issue, project: project) } - let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } - let(:commit) { project.commit } - - # Test all of {issue, merge request, commit} in both the referenced and referencing - # roles, to ensure that the correct information can be inferred from any argument. - - context 'issue from a merge request' do - subject { Note.create_cross_reference_note(issue, mergereq, author) } - - it { is_expected.to be_valid } - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(issue) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(issue.project) } - end - - describe '#author' do - subject { super().author } - it { is_expected.to eq(author) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") } - end - end - - context 'issue from a commit' do - subject { Note.create_cross_reference_note(issue, commit, author) } - - it { is_expected.to be_valid } - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(issue) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in commit #{commit.sha}") } - end - end - - context 'merge request from an issue' do - subject { Note.create_cross_reference_note(mergereq, issue, author) } - - it { is_expected.to be_valid } - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(mergereq) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(mergereq.project) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in issue ##{issue.iid}") } - end - end - - context 'commit from a merge request' do - subject { Note.create_cross_reference_note(commit, mergereq, author) } - - it { is_expected.to be_valid } - - describe '#noteable' do - subject { super().noteable } - it { is_expected.to eq(commit) } - end - - describe '#project' do - subject { super().project } - it { is_expected.to eq(project) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") } - end - end - - context 'commit contained in a merge request' do - subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author) } - - it { is_expected.to be_nil } - end - - context 'commit from issue' do - subject { Note.create_cross_reference_note(commit, issue, author) } - - it { is_expected.to be_valid } - - describe '#noteable_type' do - subject { super().noteable_type } - it { is_expected.to eq("Commit") } - end - - describe '#noteable_id' do - subject { super().noteable_id } - it { is_expected.to be_nil } - end - - describe '#commit_id' do - subject { super().commit_id } - it { is_expected.to eq(commit.id) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in issue ##{issue.iid}") } - end - end - - context 'commit from commit' do - let(:parent_commit) { commit.parents.first } - subject { Note.create_cross_reference_note(commit, parent_commit, author) } - - it { is_expected.to be_valid } - - describe '#noteable_type' do - subject { super().noteable_type } - it { is_expected.to eq("Commit") } - end - - describe '#noteable_id' do - subject { super().noteable_id } - it { is_expected.to be_nil } - end - - describe '#commit_id' do - subject { super().commit_id } - it { is_expected.to eq(commit.id) } - end - - describe '#note' do - subject { super().note } - it { is_expected.to eq("mentioned in commit #{parent_commit.id}") } - end - end - end - - describe '#cross_reference_exists?' do - let(:project) { create :project } - let(:author) { create :user } - let(:issue) { create :issue } - let(:commit0) { project.commit } - let(:commit1) { project.commit('HEAD~2') } - - before do - Note.create_cross_reference_note(issue, commit0, author) - end - - it 'detects if a mentionable has already been mentioned' do - expect(Note.cross_reference_exists?(issue, commit0)).to be_truthy - end - - it 'detects if a mentionable has not already been mentioned' do - expect(Note.cross_reference_exists?(issue, commit1)).to be_falsey - end - - context 'commit on commit' do - before do - Note.create_cross_reference_note(commit0, commit1, author) - end - - it { expect(Note.cross_reference_exists?(commit0, commit1)).to be_truthy } - it { expect(Note.cross_reference_exists?(commit1, commit0)).to be_falsey } - end - - context 'legacy note with Markdown emphasis' do - let(:issue2) { create :issue, project: project } - let!(:note) do - create :note, system: true, noteable_id: issue2.id, - noteable_type: "Issue", note: "_mentioned in issue " \ - "#{issue.project.path_with_namespace}##{issue.iid}_" - end - - it 'detects if a mentionable with emphasis has been mentioned' do - expect(Note.cross_reference_exists?(issue2, issue)).to be_truthy - end - end - end - - describe '#cross_references_with_underscores?' do - let(:project) { create :project, path: "first_project" } - let(:second_project) { create :project, path: "second_project" } - - let(:author) { create :user } - let(:issue0) { create :issue, project: project } - let(:issue1) { create :issue, project: second_project } - let!(:note) { Note.create_cross_reference_note(issue0, issue1, author) } - - it 'detects if a mentionable has already been mentioned' do - expect(Note.cross_reference_exists?(issue0, issue1)).to be_truthy - end - - it 'detects if a mentionable has not already been mentioned' do - expect(Note.cross_reference_exists?(issue1, issue0)).to be_falsey - end - - it 'detects that text has underscores' do - expect(note.note).to eq("mentioned in issue #{second_project.path_with_namespace}##{issue1.iid}") - end - end - - describe '#system?' do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:other) { create(:issue, project: project) } - let(:author) { create(:user) } - let(:assignee) { create(:user) } - let(:label) { create(:label) } - let(:milestone) { create(:milestone) } - - it 'should recognize user-supplied notes as non-system' do - @note = create(:note_on_issue) - expect(@note).not_to be_system - end - - it 'should identify status-change notes as system notes' do - @note = Note.create_status_change_note(issue, project, author, 'closed', nil) - expect(@note).to be_system - end - - it 'should identify cross-reference notes as system notes' do - @note = Note.create_cross_reference_note(issue, other, author) - expect(@note).to be_system - end - - it 'should identify assignee-change notes as system notes' do - @note = Note.create_assignee_change_note(issue, project, author, assignee) - expect(@note).to be_system - end - - it 'should identify label-change notes as system notes' do - @note = Note.create_labels_change_note(issue, project, author, [label], []) - expect(@note).to be_system - end - - it 'should identify milestone-change notes as system notes' do - @note = Note.create_milestone_change_note(issue, project, author, milestone) - expect(@note).to be_system - end - end - - describe :authorization do + describe 'authorization' do before do @p1 = create(:project) @p2 = create(:project) @@ -593,7 +155,7 @@ describe Note do @abilities << Ability end - describe :read do + describe 'read' do before do @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST) @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) @@ -604,7 +166,7 @@ describe Note do it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } end - describe :write do + describe 'write' do before do @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER) @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) @@ -615,7 +177,7 @@ describe Note do it { expect(@abilities.allowed?(@u3, :write_note, @p1)).to be_falsey } end - describe :admin do + describe 'admin' do before do @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER) @p1.project_members.create(user: @u2, access_level: ProjectMember::MASTER) @@ -631,6 +193,7 @@ describe Note do it_behaves_like 'an editable mentionable' do subject { create :note, noteable: issue, project: project } + let(:project) { create(:project) } let(:issue) { create :issue, project: project } let(:backref_text) { issue.gfm_reference } let(:set_mentionable_text) { ->(txt) { subject.note = txt } } diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index e5bf9125313..ebd8b545aa7 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -48,6 +48,21 @@ describe GitlabCiService do it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")} it { expect(@service.build_page("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232")} end + + describe "execute" do + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it "calls ci_yaml_file" do + service_hook = double + service_hook.should_receive(:execute) + @service.should_receive(:service_hook).and_return(service_hook) + @service.should_receive(:ci_yaml_file).with(push_sample_data) + + @service.execute(push_sample_data) + end + end end describe "Fork registration" do diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index bbaf54488be..e88615e1a2e 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -32,21 +32,44 @@ describe HipchatService do let(:project) { create(:project, name: 'project') } let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + let(:token) { 'verySecret' } + let(:server_url) { 'https://hipchat.example.com'} + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } before(:each) do hipchat.stub( project_id: project.id, project: project, room: 123456, - server: 'https://hipchat.example.com', - token: 'verySecret' + server: server_url, + token: token ) WebMock.stub_request(:post, api_url) end - context 'push events' do - let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + it 'should use v1 if version is provided' do + hipchat.stub(api_version: 'v1') + expect(HipChat::Client).to receive(:new). + with(token, + api_version: 'v1', + server_url: server_url). + and_return( + double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + it 'should use v2 as the version when nothing is provided' do + hipchat.stub(api_version: '') + expect(HipChat::Client).to receive(:new). + with(token, + api_version: 'v2', + server_url: server_url). + and_return( + double(:hipchat_service).as_null_object) + hipchat.execute(push_sample_data) + end + + context 'push events' do it "should call Hipchat API for push events" do hipchat.execute(push_sample_data) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 37e21a90818..48568e2a3ff 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -32,7 +32,7 @@ require 'spec_helper' describe Project do - describe 'Associations' do + describe 'associations' do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:creator).class_name('User') } @@ -54,10 +54,17 @@ describe Project do it { is_expected.to have_one(:asana_service).dependent(:destroy) } end - describe 'Mass assignment' do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::ShellAdapter) } + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe 'Validation' do + describe 'validation' do let!(:project) { create(:project) } it { is_expected.to validate_presence_of(:name) } @@ -91,6 +98,14 @@ describe Project do it { is_expected.to respond_to(:path_with_namespace) } end + describe '#to_reference' do + let(:project) { create(:empty_project) } + + it 'returns a String reference to the object' do + expect(project.to_reference).to eq project.path_with_namespace + end + end + it 'should return valid url to repo' do project = Project.new(path: 'somewhere') expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e37dcc75230..c81dd36ef4b 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -18,23 +18,47 @@ require 'spec_helper' describe Snippet do - describe "Associations" do - it { is_expected.to belong_to(:author).class_name('User') } - it { is_expected.to have_many(:notes).dependent(:destroy) } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Linguist::BlobHelper) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe "Mass assignment" do + describe 'associations' do + it { is_expected.to belong_to(:author).class_name('User') } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:notes).dependent(:destroy) } end - describe "Validation" do + describe 'validation' do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } it { is_expected.to ensure_length_of(:title).is_within(0..255) } it { is_expected.to validate_presence_of(:file_name) } - it { is_expected.to ensure_length_of(:title).is_within(0..255) } + it { is_expected.to ensure_length_of(:file_name).is_within(0..255) } it { is_expected.to validate_presence_of(:content) } + + it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) } + end + + describe '#to_reference' do + let(:project) { create(:empty_project) } + let(:snippet) { create(:snippet, project: project) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$#{snippet.id}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 771709c127a..be0b70395d6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -50,6 +50,11 @@ # bitbucket_access_token :string(255) # bitbucket_access_token_secret :string(255) # location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean +# otp_backup_codes :text # public_email :string(255) default(""), not null # @@ -58,7 +63,17 @@ require 'spec_helper' describe User do include Gitlab::CurrentSettings - describe "Associations" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::CurrentSettings) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(TokenAuthenticatable) } + end + + describe 'associations' do it { is_expected.to have_one(:namespace) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } @@ -74,9 +89,6 @@ describe User do it { is_expected.to have_many(:identities).dependent(:destroy) } end - describe "Mass assignment" do - end - describe 'validations' do it { is_expected.to validate_presence_of(:username) } it { is_expected.to validate_presence_of(:projects_limit) } @@ -170,6 +182,14 @@ describe User do it { is_expected.to respond_to(:private_token) } end + describe '#to_reference' do + let(:user) { create(:user) } + + it 'returns a String reference to the object' do + expect(user.to_reference).to eq "@#{user.username}" + end + end + describe '#generate_password' do it "should execute callback when force_random_password specified" do user = build(:user, force_random_password: true) @@ -228,6 +248,7 @@ describe User do it { expect(@user.several_namespaces?).to be_truthy } it { expect(@user.authorized_groups).to eq([@group]) } it { expect(@user.owned_groups).to eq([@group]) } + it { expect(@user.namespaces).to match_array([@user.namespace, @group]) } end describe 'group multiple owners' do @@ -250,6 +271,7 @@ describe User do end it { expect(@user.several_namespaces?).to be_falsey } + it { expect(@user.namespaces).to eq([@user.namespace]) } end describe 'blocking user' do @@ -552,7 +574,6 @@ describe User do end describe "#contributed_projects_ids" do - subject { create(:user) } let!(:project1) { create(:project) } let!(:project2) { create(:project, forked_from_project: project3) } @@ -578,4 +599,21 @@ describe User do expect(subject.contributed_projects_ids).not_to include(project2.id) end end + + describe :can_be_removed? do + subject { create(:user) } + + context 'no owned groups' do + it { expect(subject.can_be_removed?).to be_truthy } + end + + context 'has owned groups' do + before do + group = create(:group) + group.add_owner(subject) + end + + it { expect(subject.can_be_removed?).to be_falsey } + end + end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index bab8888a631..15f547e128d 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -49,10 +49,6 @@ describe API::API, api: true do } it "should create a new file in project repo" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: true, - ) - post api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(201) expect(json_response['file_path']).to eq('newfile.rb') @@ -63,9 +59,9 @@ describe API::API, api: true do expect(response.status).to eq(400) end - it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: false, + it "should return a 400 if editor fails to create file" do + Repository.any_instance.stub( + commit_file: false, ) post api("/projects/#{project.id}/repository/files", user), valid_params @@ -84,10 +80,6 @@ describe API::API, api: true do } it "should update existing file in project repo" do - Gitlab::Satellite::EditFileAction.any_instance.stub( - commit!: true, - ) - put api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(200) expect(json_response['file_path']).to eq(file_path) @@ -97,35 +89,6 @@ describe API::API, api: true do put api("/projects/#{project.id}/repository/files", user) expect(response.status).to eq(400) end - - it 'should return a 400 if the checkout fails' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::CheckoutFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(400) - - ref = valid_params[:branch_name] - expect(response.body).to match("ref '#{ref}' could not be checked out") - end - - it 'should return a 409 if the file was not modified' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::CommitFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(409) - expect(response.body).to match("Maybe there was nothing to commit?") - end - - it 'should return a 409 if the push fails' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::PushFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(409) - expect(response.body).to match("Maybe the file was changed by another process?") - end end describe "DELETE /projects/:id/repository/files" do @@ -138,10 +101,6 @@ describe API::API, api: true do } it "should delete existing file in project repo" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: true, - ) - delete api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(200) expect(json_response['file_path']).to eq(file_path) @@ -153,8 +112,8 @@ describe API::API, api: true do end it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: false, + Repository.any_instance.stub( + remove_file: false, ) delete api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 4c7d15d6594..8d0ae1475c2 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:key) { create(:key, user: user) } let(:project) { create(:project) } - let(:secret_token) { File.read Rails.root.join('.gitlab_shell_secret') } + let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file } describe "GET /internal/check", no_db: true do it do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index dcd50f73326..0ed5883914b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -349,10 +349,10 @@ describe API::API, api: true do expect(json_response['description']).to eq('New description') end - it "should return 422 when source_branch and target_branch are renamed the same" do + it "should return 400 when source_branch is specified" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), source_branch: "master", target_branch: "master" - expect(response.status).to eq(422) + expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 6ddaaa0a6dd..21787fdd895 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:admin) { create(:admin) } + let(:user) { create(:user) } let!(:group1) { create(:group) } let!(:group2) { create(:group) } @@ -14,7 +15,7 @@ describe API::API, api: true do end end - context "when authenticated as admin" do + context "when authenticated as admin" do it "admin: should return an array of all namespaces" do get api("/namespaces", admin) expect(response.status).to eq(200) @@ -22,6 +23,32 @@ describe API::API, api: true do expect(json_response.length).to eq(Namespace.count) end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{group1.name}", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + end + + context "when authenticated as a regular user" do + it "user: should return an array of namespaces" do + get api("/namespaces", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{user.username}", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cc387378d3a..dbfd72e5f19 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -57,14 +57,14 @@ describe API::API, api: true do expect(json_response.first['name']).to eq(project.name) expect(json_response.first['owner']['username']).to eq(user.username) end - + it 'should include the project labels as the tag_list' do get api('/projects', user) response.status.should == 200 json_response.should be_an Array json_response.first.keys.should include('tag_list') end - + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } @@ -86,6 +86,15 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end + + it 'returns projects in the correct order when ci_enabled_first parameter is passed' do + [project, project2, project3].each{ |project| project.build_missing_services } + project2.gitlab_ci_service.update(active: true, token: "token", project_url: "url") + get api('/projects', user), { ci_enabled_first: 'true'} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project2.id) + end end end end @@ -156,7 +165,7 @@ describe API::API, api: true do it "should assign attributes to project" do project = attributes_for(:project, { path: 'camelCasePath', - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false @@ -265,7 +274,7 @@ describe API::API, api: true do it 'should assign attributes to project' do project = attributes_for(:project, { - description: Faker::Lorem.sentence, + description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false @@ -783,11 +792,6 @@ describe API::API, api: true do describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{project.path_with_namespace}/) - ).twice - delete api("/projects/#{project.id}", user) expect(response.status).to eq(200) end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 042352311da..0040718d9be 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -172,7 +172,7 @@ end # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy describe Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :show, :new, :create] } + let(:actions) { [:index, :new, :create] } let(:controller) { 'deploy_keys' } end end @@ -208,23 +208,31 @@ describe Projects::RefsController, 'routing' do end end -# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs -# automerge_project_merge_request POST /:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge -# automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check -# branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from -# branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to -# project_merge_requests GET /:project_id/merge_requests(.:format) projects/merge_requests#index -# POST /:project_id/merge_requests(.:format) projects/merge_requests#create -# new_project_merge_request GET /:project_id/merge_requests/new(.:format) projects/merge_requests#new -# edit_project_merge_request GET /:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit -# project_merge_request GET /:project_id/merge_requests/:id(.:format) projects/merge_requests#show -# PUT /:project_id/merge_requests/:id(.:format) projects/merge_requests#update -# DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy +# diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs +# commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits +# automerge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge +# automerge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check +# ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status +# toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription +# branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from +# branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to +# update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches +# namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index +# POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create +# new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new +# edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit +# namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show +# PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update +# PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update describe Projects::MergeRequestsController, 'routing' do it 'to #diffs' do expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + it 'to #automerge' do expect(post('/gitlab/gitlabhq/merge_requests/1/automerge')).to route_to( 'projects/merge_requests#automerge', diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb new file mode 100644 index 00000000000..24e439503e7 --- /dev/null +++ b/spec/services/destroy_group_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe DestroyGroupService do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } + let!(:gitlab_shell) { Gitlab::Shell.new } + let!(:remove_path) { group.path + "+#{group.id}+deleted" } + + context 'database records' do + before do + destroy_group(group, user) + end + + it { Group.all.should_not include(group) } + it { Project.all.should_not include(project) } + end + + context 'file system' do + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed dir will be removed + Sidekiq::Testing.inline! { destroy_group(group, user) } + end + + it { gitlab_shell.exists?(group.path).should be_falsey } + it { gitlab_shell.exists?(remove_path).should be_falsey } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_group(group, user) } + end + + it { gitlab_shell.exists?(group.path).should be_falsey } + it { gitlab_shell.exists?(remove_path).should be_truthy } + end + end + + def destroy_group(group, user) + DestroyGroupService.new(group, user).execute + end +end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index d15dff1b52b..0e5ae724bf7 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Issues::CloseService do - let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:user2) { create(:user) } let(:issue) { create(:issue, assignee: user2) } + let(:project) { issue.project } before do project.team << [user, :master] diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 22b89bec96d..a91be3b4472 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,18 +1,18 @@ require 'spec_helper' describe Issues::UpdateService do - let(:project) { create(:empty_project) } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:issue) { create(:issue) } + let(:issue) { create(:issue, title: 'Old title') } let(:label) { create(:label) } + let(:project) { issue.project } before do project.team << [user, :master] project.team << [user2, :developer] end - describe :execute do + describe 'execute' do context "valid params" do before do opts = { @@ -40,15 +40,32 @@ describe Issues::UpdateService do expect(email.subject).to include(issue.title) end + def find_note(starting_with) + @issue.notes.find do |note| + note && note.note.start_with?(starting_with) + end + end + it 'should create system note about issue reassign' do - note = @issue.notes.last + note = find_note('Reassigned to') + + expect(note).not_to be_nil expect(note.note).to include "Reassigned to \@#{user2.username}" end it 'should create system note about issue label edit' do - note = @issue.notes[1] + note = find_note('Added ~') + + expect(note).not_to be_nil expect(note.note).to include "Added ~#{label.id} label" end + + it 'creates system note about title change' do + note = find_note('Title changed') + + expect(note).not_to be_nil + expect(note.note).to eq 'Title changed from **Old title** to **New title**' + end end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 879df0c9c67..0f9b65678df 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -30,11 +30,18 @@ describe MergeRequests::RefreshService do end context 'push to origin repo source branch' do + let(:refresh_service) { service.new(@project, @user) } before do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') reload_mrs end + it 'should execute hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@merge_request, 'update') + end + it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request).to be_open } it { expect(@fork_merge_request).to be_open } @@ -54,11 +61,18 @@ describe MergeRequests::RefreshService do end context 'push to fork repo source branch' do + let(:refresh_service) { service.new(@fork_project, @user) } before do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') reload_mrs end + it 'should execute hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@fork_merge_request, 'update') + end + it { expect(@merge_request.notes).to be_empty } it { expect(@merge_request).to be_open } it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 916b01e1c45..c75173c1452 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::UpdateService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple) } + let(:merge_request) { create(:merge_request, :simple, title: 'Old title') } let(:project) { merge_request.project } let(:label) { create(:label) } @@ -12,7 +12,7 @@ describe MergeRequests::UpdateService do project.team << [user2, :developer] end - describe :execute do + describe 'execute' do context 'valid params' do let(:opts) do { @@ -20,7 +20,8 @@ describe MergeRequests::UpdateService do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id] + label_ids: [label.id], + target_branch: 'target' } end @@ -39,6 +40,7 @@ describe MergeRequests::UpdateService do it { expect(@merge_request).to be_closed } it { expect(@merge_request.labels.count).to eq(1) } it { expect(@merge_request.labels.first.title).to eq('Bug') } + it { expect(@merge_request.target_branch).to eq('target') } it 'should execute hooks with update action' do expect(service).to have_received(:execute_hooks). @@ -51,15 +53,39 @@ describe MergeRequests::UpdateService do expect(email.subject).to include(merge_request.title) end + def find_note(starting_with) + @merge_request.notes.find do |note| + note && note.note.start_with?(starting_with) + end + end + it 'should create system note about merge_request reassign' do - note = @merge_request.notes.last + note = find_note('Reassigned to') + + expect(note).not_to be_nil expect(note.note).to include "Reassigned to \@#{user2.username}" end it 'should create system note about merge_request label edit' do - note = @merge_request.notes[1] + note = find_note('Added ~') + + expect(note).not_to be_nil expect(note.note).to include "Added ~#{label.id} label" end + + it 'creates system note about title change' do + note = find_note('Title changed') + + expect(note).not_to be_nil + expect(note.note).to eq 'Title changed from **Old title** to **New title**' + end + + it 'creates system note about branch change' do + note = find_note('Target') + + expect(note).not_to be_nil + expect(note.note).to eq 'Target branch changed from `master` to `target`' + end end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 1a02299bf19..0dc3b412783 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -15,6 +15,8 @@ describe Notes::CreateService do noteable_id: issue.id } + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) @note = Notes::CreateService.new(project, user, opts).execute end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 2a54b2e920a..62a99d15952 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -31,7 +31,8 @@ describe NotificationService do describe 'Notes' do context 'issue note' do - let(:issue) { create(:issue, assignee: create(:user)) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced') } @@ -101,7 +102,8 @@ describe NotificationService do end context 'issue note mention' do - let(:issue) { create(:issue, assignee: create(:user)) } + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: create(:user)) } let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') } @@ -145,7 +147,8 @@ describe NotificationService do end context 'commit note' do - let(:note) { create(:note_on_commit) } + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_commit, project: project) } before do build_team(note.project) @@ -192,7 +195,8 @@ describe NotificationService do end describe 'Issues' do - let(:issue) { create :issue, assignee: create(:user), description: 'cc @participant' } + let(:project) { create(:empty_project, :public) } + let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' } before do build_team(issue.project) @@ -295,7 +299,8 @@ describe NotificationService do end describe 'Merge Requests' do - let(:merge_request) { create :merge_request, assignee: create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user) } before do build_team(merge_request.target_project) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb new file mode 100644 index 00000000000..cdf576cc0c1 --- /dev/null +++ b/spec/services/projects/destroy_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::DestroyService do + let!(:user) { create(:user) } + let!(:project) { create(:project, namespace: user.namespace) } + let!(:path) { project.repository.path_to_repo } + let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } + + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end + + it { Project.all.should_not include(project) } + it { Dir.exists?(path).should be_falsey } + it { Dir.exists?(remove_path).should be_falsey } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_project(project, user, {}) } + end + + it { Project.all.should_not include(project) } + it { Dir.exists?(path).should be_falsey } + it { Dir.exists?(remove_path).should be_truthy } + end + + def destroy_project(project, user, params) + Projects::DestroyService.new(project, user, params).execute + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb new file mode 100644 index 00000000000..700286b585a --- /dev/null +++ b/spec/services/system_note_service_spec.rb @@ -0,0 +1,381 @@ +require 'spec_helper' + +describe SystemNoteService do + let(:project) { create(:project) } + let(:author) { create(:user) } + let(:noteable) { create(:issue, project: project) } + + shared_examples_for 'a system note' do + it 'is valid' do + expect(subject).to be_valid + end + + it 'sets the noteable model' do + expect(subject.noteable).to eq noteable + end + + it 'sets the project' do + expect(subject.project).to eq project + end + + it 'sets the author' do + expect(subject.author).to eq author + end + + it 'is a system note' do + expect(subject).to be_system + end + end + + describe '.add_commits' do + subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) } + + let(:noteable) { create(:merge_request, source_project: project) } + let(:new_commits) { noteable.commits } + let(:old_commits) { [] } + let(:oldrev) { nil } + + it_behaves_like 'a system note' + + describe 'note body' do + let(:note_lines) { subject.note.split("\n").reject(&:blank?) } + + context 'without existing commits' do + it 'adds a message header' do + expect(note_lines[0]).to eq "Added #{new_commits.size} commits:" + end + + it 'adds a message line for each commit' do + new_commits.each_with_index do |commit, i| + # Skip the header + expect(note_lines[i + 1]).to eq "* #{commit.short_id} - #{commit.title}" + end + end + end + + describe 'summary line for existing commits' do + let(:summary_line) { note_lines[1] } + + context 'with one existing commit' do + let(:old_commits) { [noteable.commits.last] } + + it 'includes the existing commit' do + expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`" + end + end + + context 'with multiple existing commits' do + let(:old_commits) { noteable.commits[3..-1] } + + context 'with oldrev' do + let(:oldrev) { noteable.commits[2].id } + + it 'includes a commit range' do + expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}" + end + + it 'includes a commit count' do + expect(summary_line).to end_with " - 2 commits from branch `feature`" + end + end + + context 'without oldrev' do + it 'includes a commit range' do + expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}" + end + + it 'includes a commit count' do + expect(summary_line).to end_with " - 2 commits from branch `feature`" + end + end + + context 'on a fork' do + before do + expect(noteable).to receive(:for_fork?).and_return(true) + end + + it 'includes the project namespace' do + expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`" + end + end + end + end + end + end + + describe '.change_assignee' do + subject { described_class.change_assignee(noteable, project, author, assignee) } + + let(:assignee) { create(:user) } + + it_behaves_like 'a system note' + + context 'when assignee added' do + it 'sets the note text' do + expect(subject.note).to eq "Reassigned to @#{assignee.username}" + end + end + + context 'when assignee removed' do + let(:assignee) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'Assignee removed' + end + end + end + + describe '.change_label' do + subject { described_class.change_label(noteable, project, author, added, removed) } + + let(:labels) { create_list(:label, 2) } + let(:added) { [] } + let(:removed) { [] } + + it_behaves_like 'a system note' + + context 'with added labels' do + let(:added) { labels } + let(:removed) { [] } + + it 'sets the note text' do + expect(subject.note).to eq "Added ~#{labels[0].id} ~#{labels[1].id} labels" + end + end + + context 'with removed labels' do + let(:added) { [] } + let(:removed) { labels } + + it 'sets the note text' do + expect(subject.note).to eq "Removed ~#{labels[0].id} ~#{labels[1].id} labels" + end + end + + context 'with added and removed labels' do + let(:added) { [labels[0]] } + let(:removed) { [labels[1]] } + + it 'sets the note text' do + expect(subject.note).to eq "Added ~#{labels[0].id} and removed ~#{labels[1].id} labels" + end + end + end + + describe '.change_milestone' do + subject { described_class.change_milestone(noteable, project, author, milestone) } + + let(:milestone) { create(:milestone, project: project) } + + it_behaves_like 'a system note' + + context 'when milestone added' do + it 'sets the note text' do + expect(subject.note).to eq "Milestone changed to #{milestone.title}" + end + end + + context 'when milestone removed' do + let(:milestone) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'Milestone removed' + end + end + end + + describe '.change_status' do + subject { described_class.change_status(noteable, project, author, status, source) } + + let(:status) { 'new_status' } + let(:source) { nil } + + it_behaves_like 'a system note' + + context 'with a source' do + let(:source) { double('commit', gfm_reference: 'commit 123456') } + + it 'sets the note text' do + expect(subject.note).to eq "Status changed to #{status} by commit 123456" + end + end + + context 'without a source' do + it 'sets the note text' do + expect(subject.note).to eq "Status changed to #{status}" + end + end + end + + describe '.change_title' do + subject { described_class.change_title(noteable, project, author, 'Old title') } + + context 'when noteable responds to `title`' do + it_behaves_like 'a system note' + + it 'sets the note text' do + expect(subject.note). + to eq "Title changed from **Old title** to **#{noteable.title}**" + end + end + + context 'when noteable does not respond to `title' do + let(:noteable) { double('noteable') } + + it 'returns nil' do + expect(subject).to be_nil + end + end + end + + describe '.change_branch' do + subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } + let(:old_branch) { 'old_branch'} + let(:new_branch) { 'new_branch'} + + it_behaves_like 'a system note' + + context 'when target branch name changed' do + it 'sets the note text' do + expect(subject.note).to eq "Target branch changed from `#{old_branch}` to `#{new_branch}`" + end + end + end + + describe '.cross_reference' do + subject { described_class.cross_reference(noteable, mentioner, author) } + + let(:mentioner) { create(:issue, project: project) } + + it_behaves_like 'a system note' + + context 'when cross-reference disallowed' do + before do + expect(described_class).to receive(:cross_reference_disallowed?).and_return(true) + end + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when cross-reference allowed' do + before do + expect(described_class).to receive(:cross_reference_disallowed?).and_return(false) + end + + describe 'note_body' do + context 'cross-project' do + let(:project2) { create(:project) } + let(:mentioner) { create(:issue, project: project2) } + + context 'from Commit' do + let(:mentioner) { project2.repository.commit } + + it 'references the mentioning commit' do + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" + end + end + + context 'from non-Commit' do + it 'references the mentioning object' do + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" + end + end + end + + context 'within the same project' do + context 'from Commit' do + let(:mentioner) { project.repository.commit } + + it 'references the mentioning commit' do + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" + end + end + + context 'from non-Commit' do + it 'references the mentioning object' do + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" + end + end + end + end + end + end + + describe '.cross_reference?' do + it 'is truthy when text begins with expected text' do + expect(described_class.cross_reference?('mentioned in something')).to be_truthy + end + + it 'is falsey when text does not begin with expected text' do + expect(described_class.cross_reference?('this is a note')).to be_falsey + end + end + + describe '.cross_reference_disallowed?' do + context 'when mentioner is not a MergeRequest' do + it 'is falsey' do + mentioner = noteable.dup + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_falsey + end + end + + context 'when mentioner is a MergeRequest' do + let(:mentioner) { create(:merge_request, :simple, source_project: project) } + let(:noteable) { project.commit } + + it 'is truthy when noteable is in commits' do + expect(mentioner).to receive(:commits).and_return([noteable]) + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_truthy + end + + it 'is falsey when noteable is not in commits' do + expect(mentioner).to receive(:commits).and_return([]) + expect(described_class.cross_reference_disallowed?(noteable, mentioner)). + to be_falsey + end + end + end + + describe '.cross_reference_exists?' do + let(:commit0) { project.commit } + let(:commit1) { project.commit('HEAD~2') } + + context 'issue from commit' do + before do + # Mention issue (noteable) from commit0 + described_class.cross_reference(noteable, commit0, author) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(noteable, commit0)). + to be_truthy + end + + it 'is falsey when not already mentioned' do + expect(described_class.cross_reference_exists?(noteable, commit1)). + to be_falsey + end + end + + context 'commit from commit' do + before do + # Mention commit1 from commit0 + described_class.cross_reference(commit0, commit1, author) + end + + it 'is truthy when already mentioned' do + expect(described_class.cross_reference_exists?(commit0, commit1)). + to be_truthy + end + + it 'is falsey when not already mentioned' do + expect(described_class.cross_reference_exists?(commit1, commit0)). + to be_falsey + end + end + end +end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb new file mode 100644 index 00000000000..755964e9a3d --- /dev/null +++ b/spec/support/filter_spec_helper.rb @@ -0,0 +1,77 @@ +# Helper methods for Gitlab::Markdown filter specs +# +# Must be included into specs manually +module FilterSpecHelper + extend ActiveSupport::Concern + + # Perform `call` on the described class + # + # Automatically passes the current `project` value, if defined, to the context + # if none is provided. + # + # html - HTML String to pass to the filter's `call` method. + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns a Nokogiri::XML::DocumentFragment + def filter(html, contexts = {}) + if defined?(project) + contexts.reverse_merge!(project: project) + end + + described_class.call(html, contexts) + end + + # Run text through HTML::Pipeline with the current filter and return the + # result Hash + # + # body - String text to run through the pipeline + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns the Hash + def pipeline_result(body, contexts = {}) + contexts.reverse_merge!(project: project) + + pipeline = HTML::Pipeline.new([described_class], contexts) + pipeline.call(body) + end + + # Modify a String reference to make it invalid + # + # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get + # their word characters reversed. + # + # reference - String reference to modify + # + # Returns a String + def invalidate_reference(reference) + if reference =~ /\A(.+)?.\d+\z/ + # Integer-based reference with optional project prefix + reference.gsub(/\d+\z/) { |i| i.to_i + 1 } + elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + # SHA-based reference with optional prefix + reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + else + reference.gsub(/\w+\z/) { |v| v.reverse } + end + end + + # Stub CrossProjectReference#user_can_reference_project? to return true for + # the current test + def allow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(true) + end + + # Stub CrossProjectReference#user_can_reference_project? to return false for + # the current test + def disallow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(false) + end + + # Shortcut to Rails' auto-generated routes helpers, to avoid including the + # module + def urls + Rails.application.routes.url_helpers + end +end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 53fb6545553..d29c8a55c82 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -10,12 +10,12 @@ def common_mentionable_setup let(:mentioned_issue) { create(:issue, project: project) } let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } - let(:mentioned_commit) { project.repository.commit } + let(:mentioned_commit) { project.commit } let(:ext_proj) { create(:project, :public) } let(:ext_issue) { create(:issue, project: ext_proj) } let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) } - let(:ext_commit) { ext_proj.repository.commit } + let(:ext_commit) { ext_proj.commit } # Override to add known commits to the repository stub. let(:extra_commits) { [] } @@ -23,21 +23,19 @@ def common_mentionable_setup # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference # to this string and place it in their +mentionable_text+. let(:ref_string) do - cross = ext_proj.path_with_namespace - <<-MSG.strip_heredoc These references are new: - Issue: ##{mentioned_issue.iid} - Merge: !#{mentioned_mr.iid} - Commit: #{mentioned_commit.id} + Issue: #{mentioned_issue.to_reference} + Merge: #{mentioned_mr.to_reference} + Commit: #{mentioned_commit.to_reference} This reference is a repeat and should only be mentioned once: - Repeat: ##{mentioned_issue.iid} + Repeat: #{mentioned_issue.to_reference} These references are cross-referenced: - Issue: #{cross}##{ext_issue.iid} - Merge: #{cross}!#{ext_mr.iid} - Commit: #{cross}@#{ext_commit.short_id} + Issue: #{ext_issue.to_reference(project)} + Merge: #{ext_mr.to_reference(project)} + Commit: #{ext_commit.to_reference(project)} This is a self-reference and should not be mentioned at all: Self: #{backref_text} @@ -109,19 +107,26 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do subject.save - cross = ext_proj.path_with_namespace - - new_text = <<-MSG + new_text = <<-MSG.strip_heredoc These references already existed: - Issue: ##{mentioned_issue.iid} - Commit: #{mentioned_commit.id} + + Issue: #{mentioned_issue.to_reference} + + Commit: #{mentioned_commit.to_reference} + + --- This cross-project reference already existed: - Issue: #{cross}##{ext_issue.iid} + + Issue: #{ext_issue.to_reference(project)} + + --- These two references are introduced in an edit: - Issue: ##{new_issues[0].iid} - Cross: #{cross}##{new_issues[1].iid} + + Issue: #{new_issues[0].to_reference} + + Cross: #{new_issues[1].to_reference(project)} MSG # These three objects were already referenced, and should not receive new diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb deleted file mode 100644 index 06c39e1ada5..00000000000 --- a/spec/support/reference_filter_spec_helper.rb +++ /dev/null @@ -1,50 +0,0 @@ -# Common methods and setup for Gitlab::Markdown reference filter specs -# -# Must be included into specs manually -module ReferenceFilterSpecHelper - extend ActiveSupport::Concern - - # Shortcut to Rails' auto-generated routes helpers, to avoid including the - # module - def urls - Rails.application.routes.url_helpers - end - - # Perform `call` on the described class - # - # Automatically passes the current `project` value to the context if none is - # provided. - # - # html - String text to pass to the filter's `call` method. - # contexts - Hash context for the filter. (default: {project: project}) - # - # Returns the String text returned by the filter's `call` method. - def filter(html, contexts = {}) - contexts.reverse_merge!(project: project) - described_class.call(html, contexts) - end - - # Run text through HTML::Pipeline with the current filter and return the - # result Hash - # - # body - String text to run through the pipeline - # contexts - Hash context for the filter. (default: {project: project}) - # - # Returns the Hash of the pipeline result - def pipeline_result(body, contexts = {}) - contexts.reverse_merge!(project: project) - - pipeline = HTML::Pipeline.new([described_class], contexts) - pipeline.call(body) - end - - def allow_cross_reference! - allow_any_instance_of(described_class). - to receive(:user_can_reference_project?).and_return(true) - end - - def disallow_cross_reference! - allow_any_instance_of(described_class). - to receive(:user_can_reference_project?).and_return(false) - end -end diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb new file mode 100644 index 00000000000..58f45ff8610 --- /dev/null +++ b/spec/teaspoon_env.rb @@ -0,0 +1,178 @@ +Teaspoon.configure do |config| + # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to + # `http://localhost:3000/jasmine` to run your tests. + config.mount_at = "/teaspoon" + + # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can + # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). + # Note: Defaults to `Rails.root` if nil. + config.root = nil + + # Paths that will be appended to the Rails assets paths + # Note: Relative to `config.root`. + config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] + + # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will + # be rendered as fixtures. + config.fixture_paths = ["spec/javascripts/fixtures"] + + # SUITES + # + # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. + # + # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can + # omit various directives and the ones defined in the default suite will be used. + # + # To run a specific suite + # - in the browser: http://localhost/teaspoon/[suite_name] + # - with the rake task: rake teaspoon suite=[suite_name] + # - with the cli: teaspoon --suite=[suite_name] + config.suite do |suite| + # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for + # you -- which you can override with the directives below. This should be specified first, as it can override other + # directives. + # Note: If no version is specified, the latest is assumed. + # + # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 + suite.use_framework :jasmine, "2.2.0" + + # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These + # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" + + # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. + #suite.javascripts = [] + + # You can include your own stylesheets if you want to change how Teaspoon looks. + # Note: Spec related CSS can and should be loaded using fixtures. + #suite.stylesheets = ["teaspoon"] + + # This suites spec helper, which can require additional support files. This file is loaded before any of your test + # files are loaded. + suite.helper = "spec_helper" + + # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating + # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. + # + # Available: boot, boot_require_js + suite.boot_partial = "boot" + + # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. + suite.body_partial = "body" + + # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a + # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. + #suite.hook :fixtures, &proc{} + + # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated + # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, + # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. + #suite.expand_assets = true + end + + # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also + # be run in the default suite -- but can be focused into a more specific suite. + #config.suite :targeted do |suite| + # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" + #end + + # CONSOLE RUNNER SPECIFIC + # + # These configuration directives are applicable only when running via the rake task or command line interface. These + # directives can be overridden using the command line interface arguments or with ENV variables when using the rake + # task. + # + # Command Line Interface: + # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js + # + # Rake: + # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite + + # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. + # + # Available: :phantomjs, :selenium, :capybara_webkit + # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS + # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver + # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit + #config.driver = :phantomjs + + # Specify additional options for the driver. + # + # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS + # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver + # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit + #config.driver_options = nil + + # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be + # considered a failure. This is to avoid issues that can arise where tests stall. + #config.driver_timeout = 180 + + # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. + #config.server = nil + + # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. + #config.server_port = nil + + # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may + # want to lower this if you know it shouldn't take long to start. + #config.server_timeout = 20 + + # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have + # several suites, but in environments like CI this may not be desirable. + #config.fail_fast = true + + # Specify the formatters to use when outputting the results. + # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. + # + # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity + #config.formatters = [:dot] + + # Specify if you want color output from the formatters. + #config.color = true + + # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to + # remove them, but in verbose applications this may not be desirable. + #config.suppress_log = false + + # COVERAGE REPORTS / THRESHOLD ASSERTIONS + # + # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and + # display coverage statistics. + # + # Coverage configurations are similar to suites. You can define several, and use different ones under different + # conditions. + # + # To run with a specific coverage configuration + # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] + # - with the cli: teaspoon --coverage=[coverage_name] + + # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage + # on the CLI. + # Set this to "true" or the name of your coverage config. + #config.use_coverage = nil + + # You can have multiple coverage configs by passing a name to config.coverage. + # e.g. config.coverage :ci do |coverage| + # The default coverage config name is :default. + config.coverage do |coverage| + # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. + # + # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity + #coverage.reports = ["text-summary", "html"] + + # The path that the coverage should be written to - when there's an artifact to write to disk. + # Note: Relative to `config.root`. + #coverage.output_path = "coverage" + + # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The + # default excludes assets from vendor, gems and support libraries. + #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] + + # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any + # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. + #coverage.statements = nil + #coverage.functions = nil + #coverage.branches = nil + #coverage.lines = nil + end +end diff --git a/vendor/assets/javascripts/jasmine-fixture.js b/vendor/assets/javascripts/jasmine-fixture.js deleted file mode 100755 index 9980aec6ddb..00000000000 --- a/vendor/assets/javascripts/jasmine-fixture.js +++ /dev/null @@ -1,433 +0,0 @@ -/* jasmine-fixture - 1.3.1 - * Makes injecting HTML snippets into the DOM easy & clean! - * https://github.com/searls/jasmine-fixture - */ -(function() { - var createHTMLBlock, - __slice = [].slice; - - (function($) { - var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref; - root = (1, eval)('this'); - originalJasmineFixture = root.jasmineFixture; - originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0; - originalAffix = root.affix; - _ = function(list) { - return { - inject: function(iterator, memo) { - var item, _i, _len, _results; - _results = []; - for (_i = 0, _len = list.length; _i < _len; _i++) { - item = list[_i]; - _results.push(memo = iterator(memo, item)); - } - return _results; - } - }; - }; - root.jasmineFixture = function($) { - var $whatsTheRootOf, affix, create, jasmineFixture, noConflict; - affix = function(selectorOptions) { - return create.call(this, selectorOptions, true); - }; - create = function(selectorOptions, attach) { - var $top; - $top = null; - _(selectorOptions.split(/[ ](?![^\{]*\})(?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) { - var $el; - if (elementSelector === ">") { - return $parent; - } - $el = createHTMLBlock($, elementSelector); - if (attach || $top) { - $el.appendTo($parent); - } - $top || ($top = $el); - return $el; - }, $whatsTheRootOf(this)); - return $top; - }; - noConflict = function() { - var currentJasmineFixture, _ref1; - currentJasmineFixture = jasmine.fixture; - root.jasmineFixture = originalJasmineFixture; - if ((_ref1 = root.jasmine) != null) { - _ref1.fixture = originalJasmineDotFixture; - } - root.affix = originalAffix; - return currentJasmineFixture; - }; - $whatsTheRootOf = function(that) { - if (that.jquery != null) { - return that; - } else if ($('#jasmine_content').length > 0) { - return $('#jasmine_content'); - } else { - return $('<div id="jasmine_content"></div>').appendTo('body'); - } - }; - jasmineFixture = { - affix: affix, - create: create, - noConflict: noConflict - }; - ewwSideEffects(jasmineFixture); - return jasmineFixture; - }; - ewwSideEffects = function(jasmineFixture) { - var _ref1; - if ((_ref1 = root.jasmine) != null) { - _ref1.fixture = jasmineFixture; - } - $.fn.affix = root.affix = jasmineFixture.affix; - return afterEach(function() { - return $('#jasmine_content').remove(); - }); - }; - if ($) { - return jasmineFixture = root.jasmineFixture($); - } else { - return root.affix = function() { - var nowJQueryExists; - nowJQueryExists = window.jQuery || window.$; - if (nowJQueryExists != null) { - jasmineFixture = root.jasmineFixture(nowJQueryExists); - return affix.call.apply(affix, [this].concat(__slice.call(arguments))); - } else { - throw new Error("jasmine-fixture requires jQuery to be defined at window.jQuery or window.$"); - } - }; - } - })(window.jQuery || window.$); - - createHTMLBlock = (function() { - var bindData, bindEvents, parseAttributes, parseClasses, parseContents, parseEnclosure, parseReferences, parseVariableScope, regAttr, regAttrDfn, regAttrs, regCBrace, regClass, regClasses, regData, regDatas, regEvent, regEvents, regExclamation, regId, regReference, regTag, regTagNotContent, regZenTagDfn; - createHTMLBlock = function($, ZenObject, data, functions, indexes) { - var ZenCode, arr, block, blockAttrs, blockClasses, blockHTML, blockId, blockTag, blocks, el, el2, els, forScope, indexName, inner, len, obj, origZenCode, paren, result, ret, zc, zo; - if ($.isPlainObject(ZenObject)) { - ZenCode = ZenObject.main; - } else { - ZenCode = ZenObject; - ZenObject = { - main: ZenCode - }; - } - origZenCode = ZenCode; - if (indexes === undefined) { - indexes = {}; - } - if (ZenCode.charAt(0) === "!" || $.isArray(data)) { - if ($.isArray(data)) { - forScope = ZenCode; - } else { - obj = parseEnclosure(ZenCode, "!"); - obj = obj.substring(obj.indexOf(":") + 1, obj.length - 1); - forScope = parseVariableScope(ZenCode); - } - while (forScope.charAt(0) === "@") { - forScope = parseVariableScope("!for:!" + parseReferences(forScope, ZenObject)); - } - zo = ZenObject; - zo.main = forScope; - el = $(); - if (ZenCode.substring(0, 5) === "!for:" || $.isArray(data)) { - if (!$.isArray(data) && obj.indexOf(":") > 0) { - indexName = obj.substring(0, obj.indexOf(":")); - obj = obj.substr(obj.indexOf(":") + 1); - } - arr = ($.isArray(data) ? data : data[obj]); - zc = zo.main; - if ($.isArray(arr) || $.isPlainObject(arr)) { - $.map(arr, function(value, index) { - var next; - zo.main = zc; - if (indexName !== undefined) { - indexes[indexName] = index; - } - if (!$.isPlainObject(value)) { - value = { - value: value - }; - } - next = createHTMLBlock($, zo, value, functions, indexes); - if (el.length !== 0) { - return $.each(next, function(index, value) { - return el.push(value); - }); - } - }); - } - if (!$.isArray(data)) { - ZenCode = ZenCode.substr(obj.length + 6 + forScope.length); - } else { - ZenCode = ""; - } - } else if (ZenCode.substring(0, 4) === "!if:") { - result = parseContents("!" + obj + "!", data, indexes); - if (result !== "undefined" || result !== "false" || result !== "") { - el = createHTMLBlock($, zo, data, functions, indexes); - } - ZenCode = ZenCode.substr(obj.length + 5 + forScope.length); - } - ZenObject.main = ZenCode; - } else if (ZenCode.charAt(0) === "(") { - paren = parseEnclosure(ZenCode, "(", ")"); - inner = paren.substring(1, paren.length - 1); - ZenCode = ZenCode.substr(paren.length); - zo = ZenObject; - zo.main = inner; - el = createHTMLBlock($, zo, data, functions, indexes); - } else { - blocks = ZenCode.match(regZenTagDfn); - block = blocks[0]; - if (block.length === 0) { - return ""; - } - if (block.indexOf("@") >= 0) { - ZenCode = parseReferences(ZenCode, ZenObject); - zo = ZenObject; - zo.main = ZenCode; - return createHTMLBlock($, zo, data, functions, indexes); - } - block = parseContents(block, data, indexes); - blockClasses = parseClasses($, block); - if (regId.test(block)) { - blockId = regId.exec(block)[1]; - } - blockAttrs = parseAttributes(block, data); - blockTag = (block.charAt(0) === "{" ? "span" : "div"); - if (ZenCode.charAt(0) !== "#" && ZenCode.charAt(0) !== "." && ZenCode.charAt(0) !== "{") { - blockTag = regTag.exec(block)[1]; - } - if (block.search(regCBrace) !== -1) { - blockHTML = block.match(regCBrace)[1]; - } - blockAttrs = $.extend(blockAttrs, { - id: blockId, - "class": blockClasses, - html: blockHTML - }); - el = $("<" + blockTag + ">", blockAttrs); - el.attr(blockAttrs); - el = bindEvents(block, el, functions); - el = bindData(block, el, data); - ZenCode = ZenCode.substr(blocks[0].length); - ZenObject.main = ZenCode; - } - if (ZenCode.length > 0) { - if (ZenCode.charAt(0) === ">") { - if (ZenCode.charAt(1) === "(") { - zc = parseEnclosure(ZenCode.substr(1), "(", ")"); - ZenCode = ZenCode.substr(zc.length + 1); - } else if (ZenCode.charAt(1) === "!") { - obj = parseEnclosure(ZenCode.substr(1), "!"); - forScope = parseVariableScope(ZenCode.substr(1)); - zc = obj + forScope; - ZenCode = ZenCode.substr(zc.length + 1); - } else { - len = Math.max(ZenCode.indexOf("+"), ZenCode.length); - zc = ZenCode.substring(1, len); - ZenCode = ZenCode.substr(len); - } - zo = ZenObject; - zo.main = zc; - els = $(createHTMLBlock($, zo, data, functions, indexes)); - els.appendTo(el); - } - if (ZenCode.charAt(0) === "+") { - zo = ZenObject; - zo.main = ZenCode.substr(1); - el2 = createHTMLBlock($, zo, data, functions, indexes); - $.each(el2, function(index, value) { - return el.push(value); - }); - } - } - ret = el; - return ret; - }; - bindData = function(ZenCode, el, data) { - var datas, i, split; - if (ZenCode.search(regDatas) === 0) { - return el; - } - datas = ZenCode.match(regDatas); - if (datas === null) { - return el; - } - i = 0; - while (i < datas.length) { - split = regData.exec(datas[i]); - if (split[3] === undefined) { - $(el).data(split[1], data[split[1]]); - } else { - $(el).data(split[1], data[split[3]]); - } - i++; - } - return el; - }; - bindEvents = function(ZenCode, el, functions) { - var bindings, fn, i, split; - if (ZenCode.search(regEvents) === 0) { - return el; - } - bindings = ZenCode.match(regEvents); - if (bindings === null) { - return el; - } - i = 0; - while (i < bindings.length) { - split = regEvent.exec(bindings[i]); - if (split[2] === undefined) { - fn = functions[split[1]]; - } else { - fn = functions[split[2]]; - } - $(el).bind(split[1], fn); - i++; - } - return el; - }; - parseAttributes = function(ZenBlock, data) { - var attrStrs, attrs, i, parts; - if (ZenBlock.search(regAttrDfn) === -1) { - return undefined; - } - attrStrs = ZenBlock.match(regAttrDfn); - attrs = {}; - i = 0; - while (i < attrStrs.length) { - parts = regAttr.exec(attrStrs[i]); - attrs[parts[1]] = ""; - if (parts[3] !== undefined) { - attrs[parts[1]] = parseContents(parts[3], data); - } - i++; - } - return attrs; - }; - parseClasses = function($, ZenBlock) { - var classes, clsString, i; - ZenBlock = ZenBlock.match(regTagNotContent)[0]; - if (ZenBlock.search(regClasses) === -1) { - return undefined; - } - classes = ZenBlock.match(regClasses); - clsString = ""; - i = 0; - while (i < classes.length) { - clsString += " " + regClass.exec(classes[i])[1]; - i++; - } - return $.trim(clsString); - }; - parseContents = function(ZenBlock, data, indexes) { - var html; - if (indexes === undefined) { - indexes = {}; - } - html = ZenBlock; - if (data === undefined) { - return html; - } - while (regExclamation.test(html)) { - html = html.replace(regExclamation, function(str, str2) { - var begChar, fn, val; - begChar = ""; - if (str.indexOf("!for:") > 0 || str.indexOf("!if:") > 0) { - return str; - } - if (str.charAt(0) !== "!") { - begChar = str.charAt(0); - str = str.substring(2, str.length - 1); - } - fn = new Function("data", "indexes", "var r=undefined;" + "with(data){try{r=" + str + ";}catch(e){}}" + "with(indexes){try{if(r===undefined)r=" + str + ";}catch(e){}}" + "return r;"); - val = unescape(fn(data, indexes)); - return begChar + val; - }); - } - html = html.replace(/\\./g, function(str) { - return str.charAt(1); - }); - return unescape(html); - }; - parseEnclosure = function(ZenCode, open, close, count) { - var index, ret; - if (close === undefined) { - close = open; - } - index = 1; - if (count === undefined) { - count = (ZenCode.charAt(0) === open ? 1 : 0); - } - if (count === 0) { - return; - } - while (count > 0 && index < ZenCode.length) { - if (ZenCode.charAt(index) === close && ZenCode.charAt(index - 1) !== "\\") { - count--; - } else { - if (ZenCode.charAt(index) === open && ZenCode.charAt(index - 1) !== "\\") { - count++; - } - } - index++; - } - ret = ZenCode.substring(0, index); - return ret; - }; - parseReferences = function(ZenCode, ZenObject) { - ZenCode = ZenCode.replace(regReference, function(str) { - var fn; - str = str.substr(1); - fn = new Function("objs", "var r=\"\";" + "with(objs){try{" + "r=" + str + ";" + "}catch(e){}}" + "return r;"); - return fn(ZenObject, parseReferences); - }); - return ZenCode; - }; - parseVariableScope = function(ZenCode) { - var forCode, rest, tag; - if (ZenCode.substring(0, 5) !== "!for:" && ZenCode.substring(0, 4) !== "!if:") { - return undefined; - } - forCode = parseEnclosure(ZenCode, "!"); - ZenCode = ZenCode.substr(forCode.length); - if (ZenCode.charAt(0) === "(") { - return parseEnclosure(ZenCode, "(", ")"); - } - tag = ZenCode.match(regZenTagDfn)[0]; - ZenCode = ZenCode.substr(tag.length); - if (ZenCode.length === 0 || ZenCode.charAt(0) === "+") { - return tag; - } else if (ZenCode.charAt(0) === ">") { - rest = ""; - rest = parseEnclosure(ZenCode.substr(1), "(", ")", 1); - return tag + ">" + rest; - } - return undefined; - }; - regZenTagDfn = /([#\.\@]?[\w-]+|\[([\w-!?=:"']+(="([^"]|\\")+")? {0,})+\]|\~[\w$]+=[\w$]+|&[\w$]+(=[\w$]+)?|[#\.\@]?!([^!]|\\!)+!){0,}(\{([^\}]|\\\})+\})?/i; - regTag = /(\w+)/i; - regId = /(?:^|\b)#([\w-!]+)/i; - regTagNotContent = /((([#\.]?[\w-]+)?(\[([\w!]+(="([^"]|\\")+")? {0,})+\])?)+)/i; - /* - See lookahead syntax (?!) at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp - */ - - regClasses = /(\.[\w-]+)(?!["\w])/g; - regClass = /\.([\w-]+)/i; - regReference = /(@[\w$_][\w$_\d]+)/i; - regAttrDfn = /(\[([\w-!]+(="?([^"]|\\")+"?)? {0,})+\])/ig; - regAttrs = /([\w-!]+(="([^"]|\\")+")?)/g; - regAttr = /([\w-!]+)(="?((([\w]+(\[.*?\])+)|[^"\]]|\\")+)"?)?/i; - regCBrace = /\{(([^\}]|\\\})+)\}/i; - regExclamation = /(?:([^\\]|^))!([^!]|\\!)+!/g; - regEvents = /\~[\w$]+(=[\w$]+)?/g; - regEvent = /\~([\w$]+)=([\w$]+)/i; - regDatas = /&[\w$]+(=[\w$]+)?/g; - regData = /&([\w$]+)(=([\w$]+))?/i; - return createHTMLBlock; - })(); - -}).call(this); |