diff options
481 files changed, 6477 insertions, 2836 deletions
diff --git a/CHANGELOG b/CHANGELOG index 8cde5377766..21a935206c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,20 +1,122 @@ Please view this file on the master branch, on stable branches it's out of date. -v 7.13.0 (unreleased) +v 8.0.0 (unreleased) + +v 7.14.0 (unreleased) + - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller) + - Fix redirection after sign in when using auto_sign_in_with_provider + - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu) + - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu) + - Provide more feedback what went wrong if HipChat service failed test (Stan Hu) + - Fix bug where backslashes in inline diffs could be dropped (Stan Hu) + - Disable turbolinks when linking to Bitbucket import status (Stan Hu) + - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu) + - Fix corrupted binary files when using API files endpoint (Stan Hu) + - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu) + - Show incompatible projects in Bitbucket import status (Stan Hu) + - Fix coloring of diffs on MR Discussion-tab (Gert Goet) + - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu) + - Fix errors deleting and creating branches with encoded slashes (Stan Hu) + - Always add current user to autocomplete controller to support filter by "Me" (Stan Hu) + - Fix multi-line syntax highlighting (Stan Hu) + - Fix network graph when branch name has single quotes (Stan Hu) + - Add "Confirm user" button in user admin page (Stan Hu) + - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) + - Add support for Unicode filenames in relative links (Hiroyuki Sato) + - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) + - Fix commit data retrieval when branch name has single quotes (Stan Hu) + - Check that project was actually created rather than just validated in import:repos task (Stan Hu) + - Fix full screen mode for snippet comments (Daniel Gerhardt) + - Fix 404 error in files view after deleting the last file in a repository (Stan Hu) + - Fix the "Reload with full diff" URL button (Stan Hu) + - Fix label read access for unauthenticated users (Daniel Gerhardt) + - Fix access to disabled features for unauthenticated users (Daniel Gerhardt) + - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu) + - Fix file upload dialog for comment editing (Daniel Gerhardt) + - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) + - Return comments in created order in merge request API (Stan Hu) + - Disable internal issue tracker controller if external tracker is used (Stan Hu) + - Expire Rails cache entries after two weeks to prevent endless Redis growth + - Add support for destroying project milestones (Stan Hu) + - Allow custom backup archive permissions + - Add project star and fork count, group avatar URL and user/group web URL attributes to API + - Show who last edited a comment if it wasn't the original author + - Send notification to all participants when MR is merged. + - Add ability to manage user email addresses via the API. + - Show buttons to add license, changelog and contribution guide if they're missing. + - Tweak project page buttons. + - Disabled autocapitalize and autocorrect on login field (Daryl Chan) + - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) + - Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller) + - Remove redis-store TTL monkey patch + - Add support for CI skipped status + - Fetch code from forks to refs/merge-requests/:id/head when merge request created + - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg) + - Add "Check out branch" button to the MR page. + - Improve MR merge widget text and UI consistency. + - Improve text in MR "How To Merge" modal. + - Cache all events + - Order commits by date when comparing branches + - Fix bug causing error when the target branch of a symbolic ref was deleted + - Include branch/tag name in archive file and directory name + - Add dropzone upload progress + - Add a label for merged branches on branches page (Florent Baldino) + - Detect .mkd and .mkdn files as markdown (Ben Boeckel) + - Fix: User search feature in admin area does not respect filters + - Set max-width for README, issue and merge request description for easier read on big screens + - Update Flowdock integration to support new Flowdock API (Boyan Tabakov) + - Remove author from files view (Sven Strickroth) + - Fix infinite loop when SAML was incorrectly configured. + +v 7.13.5 + - Satellites reverted + +v 7.13.4 + - Allow users to send abuse reports + +v 7.13.3 + - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. + - Allow users to send abuse reports + - Remove satellites + - Link username to profile on Group Members page (Tom Webster) + +v 7.13.2 + - Fix randomly failed spec + - Create project services on Project creation + - Add admin_merge_request ability to Developer level and up + - Fix Error 500 when browsing projects with no HEAD (Stan Hu) + - Fix labels / assignee / milestone for the merge requests when issues are disabled + - Show the first tab automatically on MergeRequests#new + - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt) + - Fix Gmail Actions + +v 7.13.1 + - Fix: Label modifications are not reflected in existing notes and in the issue list + - Fix: Label not shown in the Issue list, although it's set through web interface + - Fix: Group/project references are linked incorrectly + - Improve documentation + - Fix of migration: Check if session_expire_delay column exists before adding the column + - Fix: ActionView::Template::Error + - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch + - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. + - Render Note field hints consistently for "new" and "edit" forms + +v 7.13.0 + - Remove repository graph log to fix slow cache updates after push event (Stan Hu) + - Only enable HSTS header for HTTPS and port 443 (Stan Hu) + - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu) - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt) - Add branch switching support for graphs (Daniel Gerhardt) - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt) - Remove link leading to a 404 error in Deploy Keys page (Stan Hu) - Add support for unlocking users in admin settings (Stan Hu) - Add Irker service configuration options (Stan Hu) - - Fix order of issues imported form GitHub (Hiroyuki Sato) + - Fix order of issues imported from GitHub (Hiroyuki Sato) - Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart) - Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI - Add `two_factor_enabled` field to admin user API (Stan Hu) - Fix invalid timestamps in RSS feeds (Rowan Wookey) - - Fix error when deleting a user who has projects (Stan Hu) - Fix downloading of patches on public merge requests when user logged out (Stan Hu) - - The password for the default administrator (root) account has been changed from "5iveL!fe" to "password". - Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu) - Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu) - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) @@ -27,24 +129,34 @@ v 7.13.0 (unreleased) - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 - Admin can edit and remove user identities - Convert CRLF newlines to LF when committing using the web editor. - - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged. + - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged. - Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled. - Show a user's Two-factor Authentication status in the administration area. - Explicit error when commit not found in the CI - - Improve performance for issue and merge request pages + - Improve performance for issue and merge request pages - Users with guest access level can not set assignee, labels or milestones for issue and merge request - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels - Better performance for pages with events list, issues list and commits list - - Faster automerge check and merge itself when source and target branches are in same repository + - Faster automerge check and merge itself when source and target branches are in same repository - Correctly show anonymous authorized applications under Profile > Applications. - Query Optimization in MySQL. - Allow users to be blocked and unblocked via the API - Use native Postgres database cleaning during backup restore + - Redesign project page. Show README as default instead of activity. Move project activity to separate page + - Make left menu more hierarchical and less contextual by adding back item at top + - A fork can’t have a visibility level that is greater than the original project. + - Faster code search in repository and wiki. Fixes search page timeout for big repositories + - Allow administrators to disable 2FA for a specific user + - Add error message for SSH key linebreaks + - Store commits count in database (will populate with valid values only after first push) + - Rebuild cache after push to repository in background job + - Fix transferring of project to another group using the API. v 7.12.2 - Correctly show anonymous authorized applications under Profile > Applications. - Faster automerge check and merge itself when source and target branches are in same repository - Audit log for user authentication + - Allow custom label to be set for authentication providers. v 7.12.1 - Fix error when deleting a user who has projects (Stan Hu) @@ -52,7 +164,7 @@ v 7.12.1 - Add SAML to list of social_provider (Matt Firtion) - Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets) - Fix closed merge request scope at milestone page (Dmitriy Zaporozhets) - - Revert merge request states renaming + - Revert merge request states renaming - Fix hooks for web based events with external issue references (Daniel Gerhardt) - Improve performance for issue and merge request pages - Compress database dumps to reduce backup size @@ -108,12 +220,12 @@ v 7.12.0 - 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.yml in each push call - When remove project - move repository and schedule it removal - Improve group removing logic - Trigger create-hooks on backup restore task - Add option to automatically link omniauth and LDAP identities + - Allow special character in users bio. I.e.: I <3 GitLab v 7.11.4 - Fix missing bullets when creating lists @@ -132,9 +244,6 @@ v 7.11.1 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) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index ec1cf33c3f6..2714f5313ae 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.3 +2.6.4 @@ -24,7 +24,7 @@ gem 'omniauth-shibboleth' gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-gitlab' gem 'omniauth-bitbucket' -gem 'omniauth-saml' +gem 'omniauth-saml', '~> 1.4.0' gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" @@ -38,7 +38,7 @@ gem "browser", '~> 0.8.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.5' +gem "gitlab_git", '~> 7.2.14' # Ruby/Rack Git Smart-HTTP Server Handler # GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) @@ -46,7 +46,7 @@ gem "gitlab_git", '~> 7.2.5' gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # LDAP Auth -# GitLab fork with several improvements to original library. For full list of changes +# 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" @@ -54,9 +54,9 @@ gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" 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 +# 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" @@ -150,7 +150,7 @@ gem 'tinder', '~> 1.9.2' gem 'hipchat', '~> 1.5.0' # Flowdock integration -gem "gitlab-flowdock-git-hook", "~> 0.4.2" +gem "gitlab-flowdock-git-hook", "~> 1.0.1" # Gemnasium integration gem "gemnasium-gitlab-service", "~> 0.2" @@ -203,7 +203,7 @@ gem 'jquery-ui-rails' gem 'nprogress-rails' gem 'raphael-rails', '~> 2.1.2' gem 'request_store' -gem 'select2-rails' +gem 'select2-rails', '~> 3.5.9' gem 'virtus' group :development do @@ -227,30 +227,24 @@ end group :development, :test do gem 'awesome_print' - gem 'byebug' + gem 'byebug', platform: :mri gem 'fuubar', '~> 2.0.0' gem 'pry-rails' - gem 'coveralls', require: false + gem 'coveralls', '~> 0.8.2', require: false gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails' - gem 'rspec-rails', '~> 3.3.0' - gem 'rubocop', '0.28.0', require: false + gem 'rspec-rails', '~> 3.3.0' + gem 'rubocop', '0.28.0', require: false gem 'spinach-rails' - # rest-client is a coveralls dependency and not used directly in GitLab, but - # we specify a version here to pick up some security fixes. - # See https://github.com/rest-client/rest-client/issues/369 - # and http://www.osvdb.org/show/osvdb/117461 - gem 'rest-client', '~> 1.8.0' - # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.3.0' # Generate Fake data gem 'ffaker', '~> 2.0.0' - gem 'capybara', '~> 2.3.0' + gem 'capybara', '~> 2.4.0' gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.6.0' @@ -278,4 +272,3 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' -gem "rugments", "~> 1.0.0.beta8" diff --git a/Gemfile.lock b/Gemfile.lock index 7641d908131..f0c661fa9c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM columnize (~> 0.8) debugger-linecache (~> 1.2) cal-heatmap-rails (0.0.1) - capybara (2.3.0) + capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -154,7 +154,7 @@ GEM doorkeeper (2.1.3) railties (>= 3.2) dotenv (0.9.0) - dropzonejs-rails (0.4.14) + dropzonejs-rails (0.7.1) rails (> 3.1) email_spec (1.6.0) launchy (~> 2.1) @@ -183,6 +183,9 @@ GEM ffi (1.9.8) fission (0.5.0) CFPropertyList (~> 2.2) + flowdock (0.7.0) + httparty (~> 0.7) + multi_json fog (1.25.0) fog-brightbox (~> 0.4) fog-core (~> 1.25) @@ -255,7 +258,8 @@ GEM racc github-markup (1.3.1) posix-spawn (~> 0.3.8) - gitlab-flowdock-git-hook (0.4.2.2) + gitlab-flowdock-git-hook (1.0.1) + flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json gitlab-grack (2.0.2) @@ -271,7 +275,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.2.5) + gitlab_git (7.2.14) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -288,7 +292,7 @@ GEM github-markup (~> 1.3.1) gollum-grit_adapter (~> 0.1, >= 0.1.1) nokogiri (~> 1.6.4) - rouge (~> 1.7.4) + rouge (~> 1.9) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (5.0.1) @@ -307,7 +311,7 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - haml (4.0.5) + haml (4.0.7) tilt haml-rails (0.5.3) actionpack (>= 4.0.1) @@ -373,7 +377,7 @@ GEM mini_portile (0.6.2) minitest (5.3.5) mousetrap-rails (1.4.6) - multi_json (1.11.1) + multi_json (1.11.2) multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.16) @@ -422,9 +426,9 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) - omniauth-saml (1.3.1) + omniauth-saml (1.4.1) omniauth (~> 1.1) - ruby-saml (~> 0.8.1) + ruby-saml (~> 1.0.0) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) @@ -508,7 +512,7 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.3.2) - redis (3.1.0) + redis (3.2.1) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -525,7 +529,7 @@ GEM redis-actionpack (~> 4) redis-activesupport (~> 4) redis-store (~> 1.1.0) - redis-store (1.1.4) + redis-store (1.1.6) redis (>= 2.2) request_store (1.0.5) rerun (0.10.0) @@ -536,7 +540,7 @@ GEM netrc (~> 0.7) rinku (1.7.3) rotp (1.6.1) - rouge (1.7.7) + rouge (1.9.1) rqrcode (0.4.2) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) @@ -568,8 +572,8 @@ 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) + ruby-saml (1.0.0) + nokogiri (>= 1.5.10) uuid (~> 2.3) ruby2ruby (2.1.3) ruby_parser (~> 3.1) @@ -579,7 +583,6 @@ GEM rubyntlm (0.5.0) rubypants (0.2.0) rugged (0.22.2) - rugments (1.0.0.beta8) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -598,7 +601,7 @@ GEM seed-fu (2.3.5) activerecord (>= 3.1, < 4.3) activesupport (>= 3.1, < 4.3) - select2-rails (3.5.2) + select2-rails (3.5.9.3) thor (~> 0.14) settingslogic (2.0.9) sexp_processor (4.4.5) @@ -703,14 +706,14 @@ GEM underscore-rails (1.4.4) unf (0.1.4) unf_ext - unf_ext (0.0.6) + unf_ext (0.0.7.1) unicorn (4.6.3) kgio (~> 2.6) rack raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) - uuid (2.3.7) + uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) virtus (1.0.1) @@ -753,13 +756,13 @@ DEPENDENCIES browser (~> 0.8.0) byebug cal-heatmap-rails (~> 0.0.1) - capybara (~> 2.3.0) + capybara (~> 2.4.0) capybara-screenshot (~> 1.0.0) carrierwave charlock_holmes coffee-rails colored - coveralls + coveralls (~> 0.8.2) creole (~> 0.3.6) d3_rails (~> 3.5.5) database_cleaner (~> 1.4.0) @@ -780,11 +783,11 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) github-markup - gitlab-flowdock-git-hook (~> 0.4.2) + gitlab-flowdock-git-hook (~> 1.0.1) gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.5) + gitlab_git (~> 7.2.14) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -814,7 +817,7 @@ DEPENDENCIES omniauth-gitlab omniauth-google-oauth2 omniauth-kerberos - omniauth-saml + omniauth-saml (~> 1.4.0) omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.12) @@ -833,16 +836,14 @@ DEPENDENCIES redis-rails request_store rerun (~> 0.10.0) - rest-client (~> 1.8.0) rqrcode-rails3 rspec-rails (~> 3.3.0) rubocop (= 0.28.0) - rugments (~> 1.0.0.beta8) sanitize (~> 2.0) sass-rails (~> 4.0.5) sdoc seed-fu - select2-rails + select2-rails (~> 3.5.9) settingslogic shoulda-matchers (~> 2.8.0) sidekiq (~> 3.3) @@ -878,4 +879,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.4 + 1.10.6 diff --git a/README.md b/README.md index 2e2028c97f6..52e12bb66ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ +# GitLab + +[](https://ci.gitlab.com/projects/1?ref=master) +[](https://semaphoreci.com/gitlabhq/gitlabhq) +[](https://codeclimate.com/github/gitlabhq/gitlabhq) +[](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) + ## Canonical source The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. -#  GitLab - ## Open source software to collaborate on code To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). @@ -17,21 +22,12 @@ To see how GitLab looks please see the [features page on our website](https://ab ## Editions -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 use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). - -## Code status - -- [](https://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) +There are two editions of GitLab: -- [](https://semaphoreapp.com/gitlabhq/gitlabhq) +- GitLab Community Edition (CE) is available freely under the MIT Expat license. +- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). -- [](https://codeclimate.com/github/gitlabhq/gitlabhq) - -- [](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) +Included with the GitLab Omnibus Packages is [GitLab CI](https://about.gitlab.com/gitlab-ci/) that can easily build, test and deploy code. ## Website @@ -46,23 +42,39 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a ## Requirements -GitLab requires the following software: - -- Ubuntu/Debian/CentOS/RHEL -- Ruby (MRI) 2.0 or 2.1 -- Git 1.7.10+ -- Redis 2.0+ -- MySQL or PostgreSQL - Please see the [requirements documentation](doc/install/requirements.md) for system requirements and more information about the supported operating systems. ## Installation -The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to an installation from source, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. +The recommended way to install GitLab is with the [Omnibus packages](https://about.gitlab.com/downloads/) on our package server. +Compared to an installation from source, this is faster and less error prone. +Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information. -You can access a new installation with the login **`root`** and password **`password`**, after login you are required to set a unique password. +You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password. + +## Install a development environment + +To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). +If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. +One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: + + cp config/unicorn.rb.example.development config/unicorn.rb + +Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). + +## Software stack + +GitLab is a Ruby on Rails application that runs on the following software: + +- Ubuntu/Debian/CentOS/RHEL +- Ruby (MRI) 2.1 +- Git 1.7.10+ +- Redis 2.0+ +- MySQL or PostgreSQL + +For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html). ## Third-party applications @@ -76,16 +88,6 @@ Since 2011 a minor or major version of GitLab is released on the 22nd of every m For upgrading information please see our [update page](https://about.gitlab.com/update/). -## Install a development environment - -To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). -If you do not use the GitLab Development Kit you need to install and setup all the dependencies yourself, this is a lot of work and error prone. -One small thing you also have to do when installing it yourself is to copy the example development unicorn configuration file: - - cp config/unicorn.rb.example.development config/unicorn.rb - -Instructions on how to start GitLab and how to run the tests can be found in the [development section of the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit#development). - ## Documentation All documentation can be found on [doc.gitlab.com/ce/](http://doc.gitlab.com/ce/). @@ -101,4 +103,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.
\ No newline at end of file +[These people](https://twitter.com/gitlab/favorites) seem to like it. @@ -1 +1 @@ -7.13.0.pre +8.0.0.pre diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png Binary files differindex 4b90a57bc7d..4b90a57bc7d 100644 --- a/app/assets/images/authbuttons/bitbucket_64.png +++ b/app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/github_64.png b/app/assets/images/auth_buttons/github_64.png Binary files differnew file mode 100644 index 00000000000..182a1a3f734 --- /dev/null +++ b/app/assets/images/auth_buttons/github_64.png diff --git a/app/assets/images/auth_buttons/gitlab_64.png b/app/assets/images/auth_buttons/gitlab_64.png Binary files differnew file mode 100644 index 00000000000..99a40583b3a --- /dev/null +++ b/app/assets/images/auth_buttons/gitlab_64.png diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/auth_buttons/google_64.png Binary files differindex fb64f8bee68..fb64f8bee68 100644 --- a/app/assets/images/authbuttons/google_64.png +++ b/app/assets/images/auth_buttons/google_64.png diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/auth_buttons/twitter_64.png Binary files differindex e3bd9169a34..e3bd9169a34 100644 --- a/app/assets/images/authbuttons/twitter_64.png +++ b/app/assets/images/auth_buttons/twitter_64.png diff --git a/app/assets/images/authbuttons/github_64.png b/app/assets/images/authbuttons/github_64.png Binary files differdeleted file mode 100644 index dc7c03d1005..00000000000 --- a/app/assets/images/authbuttons/github_64.png +++ /dev/null diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png Binary files differdeleted file mode 100644 index 31281a19444..00000000000 --- a/app/assets/images/authbuttons/gitlab_64.png +++ /dev/null diff --git a/app/assets/images/msapplication-tile.png b/app/assets/images/msapplication-tile.png Binary files differnew file mode 100644 index 00000000000..58bbf2b20cb --- /dev/null +++ b/app/assets/images/msapplication-tile.png diff --git a/app/assets/images/touch-icon-ipad-retina.png b/app/assets/images/touch-icon-ipad-retina.png Binary files differnew file mode 100644 index 00000000000..feb32b48ec9 --- /dev/null +++ b/app/assets/images/touch-icon-ipad-retina.png diff --git a/app/assets/images/touch-icon-ipad.png b/app/assets/images/touch-icon-ipad.png Binary files differnew file mode 100644 index 00000000000..a6ddc543509 --- /dev/null +++ b/app/assets/images/touch-icon-ipad.png diff --git a/app/assets/images/touch-icon-iphone-retina.png b/app/assets/images/touch-icon-iphone-retina.png Binary files differnew file mode 100644 index 00000000000..8bf7ccb7534 --- /dev/null +++ b/app/assets/images/touch-icon-iphone-retina.png diff --git a/app/assets/images/touch-icon-iphone.png b/app/assets/images/touch-icon-iphone.png Binary files differnew file mode 100644 index 00000000000..87da550f8be --- /dev/null +++ b/app/assets/images/touch-icon-iphone.png diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bb120424ccf..bb0a0c51fd4 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -40,6 +40,7 @@ #= require shortcuts_issuable #= require shortcuts_network #= require cal-heatmap +#= require jquery.nicescroll.min #= require_tree . window.slugify = (text) -> @@ -104,6 +105,8 @@ if location.hash window.addEventListener "hashchange", shiftWindow $ -> + $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") + # Click a .js-select-on-focus field, select the contents $(".js-select-on-focus").on "focusin", -> # Prevent a mouseup event from deselecting the input @@ -161,9 +164,10 @@ $ -> $('.account-box').hover -> $(@).toggleClass('hover') # Commit show suppressed diff - $(".diff-content").on "click", ".supp_diff_link", -> - $(@).next('table').show() - $(@).remove() + $(document).on 'click', '.diff-content .js-show-suppressed-diff', -> + $container = $(@).parent() + $container.next('table').show() + $container.remove() $('.navbar-toggle').on 'click', -> $('.header-content .title').toggle() diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 069f91c30e1..6d9b364cb8d 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -31,6 +31,10 @@ class @Diff bottom: unfoldBottom offset: offset unfold: unfold + # indent is used to compensate for single space indent to fit + # '+' and '-' prepended to diff lines, + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 + indent: 1 $.get(link, params, (response) => target.parent().replaceWith(response) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 8ceaef81a07..81e73799271 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -63,7 +63,6 @@ class Dispatcher when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() when 'projects:activity' - new Activities() shortcut_handler = new ShortcutsNavigation() when 'projects:show' shortcut_handler = new ShortcutsNavigation() @@ -129,7 +128,10 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.wiki-form')) - when 'snippets', 'labels', 'graphs' + when 'snippets' + shortcut_handler = new ShortcutsNavigation() + new ZenMode() if path[2] == 'show' + when 'labels', 'graphs' shortcut_handler = new ShortcutsNavigation() when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index a7476146010..a0dcaa8c27a 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -8,6 +8,7 @@ class @DropzoneInput divAlert = "<div class=\"" + alertClass + "\"></div>" iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>" iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" + uploadProgress = $("<div class=\"div-dropzone-progress\"></div>") btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" project_uploads_path = window.project_uploads_path or null markdown_preview_path = window.markdown_preview_path or null @@ -25,10 +26,11 @@ class @DropzoneInput form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" form_dropzone.append divHover - $(".div-dropzone-hover").append iconPaperclip + form_dropzone.find(".div-dropzone-hover").append iconPaperclip form_dropzone.append divSpinner - $(".div-dropzone-spinner").append iconSpinner - $(".div-dropzone-spinner").css + form_dropzone.find(".div-dropzone-spinner").append iconSpinner + form_dropzone.find(".div-dropzone-spinner").append uploadProgress + form_dropzone.find(".div-dropzone-spinner").css "opacity": 0 "display": "none" @@ -112,13 +114,18 @@ class @DropzoneInput $(".div-dropzone-alert").append btnAlert + errorMessage return + totaluploadprogress: (totalUploadProgress) -> + uploadProgress.text Math.round(totalUploadProgress) + "%" + return + sending: -> form_dropzone.find(".div-dropzone-spinner").css "opacity": 0.7 "display": "inherit" return - complete: -> + queuecomplete: -> + uploadProgress.text "" $(".dz-preview").remove() $(".markdown-area").trigger "input" $(".div-dropzone-spinner").css diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee index a8b3c1fa33e..e604e6025c2 100644 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -70,7 +70,7 @@ class @LineHighlighter @clearHighlight() - lineNumber = $(event.target).data('line-number') + lineNumber = $(event.target).closest('a').data('line-number') current = @hashToRange(@_hash) unless current[0] && event.shiftKey diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 7462975bd3d..b21cb7904b5 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -15,9 +15,7 @@ class @MergeRequest this.$('.show-all-commits').on 'click', => this.showAllCommits() - # `MergeRequests#new` has no tab-persisting or lazy-loading behavior - unless @opts.action == 'new' - new MergeRequestTabs(@opts) + @initTabs() # Prevent duplicate event bindings @disableTaskList() @@ -29,6 +27,14 @@ class @MergeRequest $: (selector) -> this.$el.find(selector) + initTabs: -> + if @opts.action != 'new' + # `MergeRequests#new` has no tab-persisting or lazy-loading behavior + new MergeRequestTabs(@opts) + else + # Show the first tab (Commits) + $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') + showAllCommits: -> this.$('.first-commits').remove() this.$('.all-commits').removeClass 'hide' diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index a132a0a9dcc..19a07b6a033 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -49,12 +49,6 @@ class @MergeRequestTabs # Store the `location` object, allowing for easier stubbing in tests @_location = location - switch @opts.action - when 'commits' - @commitsLoaded = true - when 'diffs' - @diffsLoaded = true - @bindEvents() @activateTab(@opts.action) @@ -102,7 +96,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs)\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -133,7 +127,7 @@ class @MergeRequestTabs return if @diffsLoaded @_get - url: "#{source}.json" + url: "#{source}.json" + @_location.search success: (data) => document.getElementById('diffs').innerHTML = data.html @diffsLoaded = true diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index e4d815bb4e4..5ab91261d75 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -36,7 +36,7 @@ class @MergeRequestWidget showCiState: (state) -> $('.ci_widget').hide() - allowed_states = ["failed", "canceled", "running", "pending", "success", "not_found"] + allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states $('.ci_widget.ci-' + state).show() switch state @@ -49,10 +49,8 @@ class @MergeRequestWidget @setMergeButtonClass('btn-danger') showCiCoverage: (coverage) -> - cov_html = $('<span>') - cov_html.addClass('ci-coverage') - cov_html.text('Coverage ' + coverage + '%') - $('.ci_widget:visible').append(cov_html) + text = 'Coverage ' + coverage + '%' + $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> $('.accept_merge_request').removeClass("btn-create").addClass(css_class) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 1c05a2b9fe8..0021d17d85e 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -10,7 +10,6 @@ class @Notes constructor: (notes_url, note_ids, last_fetched_at, view) -> @notes_url = notes_url - @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? @note_ids = note_ids @last_fetched_at = last_fetched_at @view = view @@ -298,7 +297,7 @@ class @Notes note.find(".note-header").hide() base_form = note.find(".note-edit-form") form = base_form.clone().insertAfter(base_form) - form.addClass('current-note-edit-form') + form.addClass('current-note-edit-form gfm-form') form.find('.div-dropzone').remove() # Show the attachment delete link diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index fe83dc0410e..d639303aed3 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -12,7 +12,7 @@ @loading.show() $.ajax type: "GET" - url: location.href + url: $(".content_list").data('href') || location.href data: "limit=" + @limit + "&offset=" + @offset complete: => @loading.hide() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index eb8c1fa1426..39a433dfc91 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,12 +1,12 @@ class @Project constructor: -> # Git clone panel switcher - scope = $ '.git-clone-holder' - if scope.length > 0 - $('a, button', scope).click -> - $('a, button', scope).removeClass 'active' + cloneHolder = $('.git-clone-holder') + if cloneHolder.length + $('a, button', cloneHolder).click -> + $('a, button', cloneHolder).removeClass 'active' $(@).addClass 'active' - $('#project_clone', scope).val $(@).data 'clone' + $('#project_clone', cloneHolder).val $(@).data 'clone' $(".clone").text("").append $(@).data 'clone' # Ref switcher diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 08cbe911672..7beef1845ef 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -70,7 +70,7 @@ font-family: $monospace_font; white-space: pre; word-wrap: normal; - padding: 0; + padding: 1px 2px; } kbd { @@ -109,7 +109,7 @@ font-size: 1.2em; } - blockquote p { + blockquote { color: #888; font-size: 15px; line-height: 1.5; diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 08f153dfbc9..cb439a0e0bf 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -13,6 +13,7 @@ $code_line_height: 1.5; $border-color: #E5E5E5; $background-color: #f5f5f5; $header-height: 50px; +$readable-width: 1100px; /* diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 961ac793de2..d36530169a9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -184,7 +184,7 @@ li.note { } } -.supp_diff_link, +.show-suppressed-diff, .show-all-commits { cursor: pointer; } diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 8014dcb165b..f845342c67b 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -90,12 +90,7 @@ border-right: none; } background: #fff; - padding: 5px; - } - .author, - .blame_commit { - background: $background-color; - vertical-align: top; + padding: 8px; } .lines { pre { diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index f94677d1925..a4fc82e90bf 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -40,6 +40,15 @@ font-size: inherit; } + .div-dropzone-progress { + position: absolute; + top: 7px; + left: -40px; + width: 35px; + font-size: 13px; + text-align: right; + } + .dz-preview { display: none; } diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 24c828e0d97..bb7b9356c70 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -48,6 +48,10 @@ display: block; } + .project-home-desc { + font-size: 21px; + } + .project-repo-buttons, .git-clone-holder { display: none; diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index 00e0534b81e..b96664d30db 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -2,6 +2,9 @@ .sidebar-wrapper { position: fixed; top: 0; + bottom: 0; + overflow-y: auto; + overflow-x: hidden; left: 0; height: 100%; transition-duration: .3s; @@ -21,8 +24,9 @@ } .nav-sidebar { + margin-top: 29 + $header-height; + margin-bottom: 50px; transition-duration: .3s; - margin: 0; list-style: none; overflow: hidden; @@ -39,12 +43,12 @@ } a { + padding: 8px 15px; + font-size: 13px; + line-height: 18px; color: $gray; display: block; text-decoration: none; - padding: 8px 15px; - font-size: 14px; - line-height: 20px; padding-left: 16px; &:hover { @@ -88,14 +92,17 @@ width: $sidebar_width; .nav-sidebar { - margin-top: 29px; - position: fixed; - top: $header-height; width: $sidebar_width; } .nav-sidebar li a{ width: 230px; + + &.back-link { + i { + visibility: hidden; + } + } } } } @@ -108,15 +115,9 @@ width: $sidebar_collapsed_width; .nav-sidebar { - margin-top: 29px; - position: fixed; - top: $header-height; width: $sidebar_collapsed_width; li a { - font-size: 14px; - padding: 8px 15px; - text-align: left; padding-left: 16px; } } @@ -175,7 +176,7 @@ } .sidebar-user { - position: absolute; + position: fixed; bottom: 0; width: $sidebar_width; padding: 10px; diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 66767cb13cb..34b4ee3e17e 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -17,6 +17,14 @@ pre { background: #333; color: $background-color; } + + &.plain-readme { + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 14px; + } } .monospace { @@ -30,6 +38,10 @@ code { } } +a > code { + color: $link-color; +} + /** * Wiki typography * diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index af6ea58382f..1557c243db5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -65,6 +65,17 @@ color: #777; } + .suppressed-container { + padding: ($padding-base-vertical + 5px) $padding-base-horizontal; + text-align: center; + + // "Changes suppressed. Click to show." link + .show-suppressed-diff { + font-size: 110%; + font-weight: bold; + } + } + table { width: 100%; font-family: $monospace_font; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 586e7b5f8da..3f617e72b02 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -45,3 +45,9 @@ .btn { font-size: 13px; } } + +.issuable-details { + .description { + max-width: $readable-width; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 61071320973..10fce5b3daa 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -1,9 +1,15 @@ - - /** - * MR -> show: Automerge widget +/** + * MR -> show: Automerge widget * */ .mr-state-widget { + background: #FAFAFA; + margin-bottom: 20px; + color: #666; + border: 1px solid #e5e5e5; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); + @include border-radius(3px); + form { margin-bottom: 0; .clearfix { @@ -20,22 +26,71 @@ display: inline-block; margin: 0; margin-left: 20px; - padding: 10px 0; + padding: 5px; line-height: 20px; - font-weight: bold; .remove_source_checkbox { margin: 0; - font-weight: bold; } } } + + .ci_widget { + border-bottom: 1px solid #EEE; + + i { + margin-right: 4px; + } + + &.ci-success { + color: $gl-success; + } + + &.ci-skipped { + background-color: #eee; + color: #888; + } + + &.ci-pending, + &.ci-running { + color: $gl-warning; + } + + &.ci-failed, + &.ci-canceled, + &.ci-error { + color: $gl-danger; + } + } + + .mr-widget-body, + .ci_widget, + .mr-widget-footer { + padding: 15px; + } + + .mr-widget-body { + h4 { + font-weight: bold; + margin: 5px 0; + } + + p:last-child { + margin-bottom: 0; + } + } + + .mr-widget-footer { + border-top: 1px solid #EEE; + } + + .ci-coverage { + float: right; + } } @media(min-width: $screen-sm-max) { .merge-request .merge-request-tabs{ - margin: 20px 0; - li { a { padding: 15px 40px; @@ -45,6 +100,11 @@ } } +.merge-request .merge-request-tabs{ + margin-top: 30px; + margin-bottom: 20px; +} + .mr_source_commit, .mr_target_commit { .commit { @@ -58,23 +118,10 @@ } .label-branch { - @include border-radius(4px); - padding: 3px 4px; - border: none; - background: $hover; - color: #333; + color: #222; font-family: $monospace_font; - font-weight: normal; + font-weight: bold; overflow: hidden; - - .label-project { - @include border-radius-left(4px); - padding: 3px 4px; - background: #279; - position: relative; - left: -4px; - letter-spacing: -1px; - } } .mr-list { @@ -121,59 +168,6 @@ display: none; } -.mr-state-widget { - font-size: 13px; - background: #FAFAFA; - margin-bottom: 20px; - color: #666; - 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 #EEE; - - &.ci-success { - color: $gl-success; - } - - &.ci-pending, - &.ci-running { - color: $gl-warning; - } - - &.ci-failed, - &.ci-canceled, - &.ci-error { - color: $gl-danger; - } - } - - .mr-widget-body { - padding: 10px 15px; - - h4 { - font-weight: bold; - margin: 5px 0; - } - - p:last-child { - margin-bottom: 0; - } - } - - .mr-widget-footer { - padding: 10px 15px; - border-top: 1px solid #EEE; - } - - .ci-coverage { - float: right; - } -} - .merge-request-show-labels { a { margin-right: 5px; @@ -188,3 +182,11 @@ .merge-request-form .select2-container { width: 250px !important; } + +#modal_merge_info .modal-dialog { + width: 600px; +} + +.mr-source-target { + line-height: 31px; +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 42b8ecabb38..85c828ec1ad 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -37,7 +37,7 @@ ul.notes { font-size: 13px; a { - @extend .cgray; + @extend .cgray; &:hover { text-decoration: underline; @@ -72,13 +72,28 @@ ul.notes { .note { display: block; position:relative; + .note-body { overflow: auto; + .note-text { overflow: auto; word-wrap: break-word; @include md-typography; + // Reset ul style types since we're nested inside a ul already + & > ul { + list-style-type: disc; + + ul { + list-style-type: circle; + + ul { + list-style-type: square; + } + } + } + // Reduce left padding of first task list ul element ul.task-list:first-child { padding-left: 10px; @@ -90,10 +105,13 @@ ul.notes { } hr { + // Darken 'whitesmoke' a bit to make it more visible in note bodies + border-color: darken(#F5F5F5, 8%); margin: 10px 0; } } } + .note-header { padding-bottom: 3px; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 5f415f2d78f..29d3dbc25eb 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -16,7 +16,6 @@ .project-home-panel { text-align: center; - margin-bottom: 20px; .project-identicon-holder { margin-bottom: 15px; @@ -31,7 +30,13 @@ } } - .lead { + .project-home-desc { + h1 { + margin: 0; + margin-bottom: 10px; + font-size: 26px; + } + p { display: inline; } @@ -39,7 +44,7 @@ .git-clone-holder { max-width: 600px; - margin: 0 auto; + margin: 20px auto; } .visibility-level-label { @@ -297,6 +302,15 @@ table.table.protected-branches-list tr.no-border { ul.nav-pills { display:inline-block; } li { display:inline; } a { float:left; } + + li.missing a { + color: #bbb; + border: 1px dashed #ccc; + + &:hover { + background-color: #FAFAFA; + } + } } pre.light-well { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 34ee4d7b31e..5f1a3db4fb6 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -89,6 +89,10 @@ td.blame-commit { background: #f9f9f9; min-width: 350px; + + .commit-author-link { + color: #888; + } } td.blame-numbers { pre { @@ -112,6 +116,9 @@ } .readme-holder { + margin: 0 auto; + max-width: $readable-width; + .readme-file-title { font-size: 14px; font-weight: bold; diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss index dc47b108100..3589cb88d03 100644 --- a/app/assets/stylesheets/themes/gitlab-theme.scss +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -35,9 +35,9 @@ .sidebar-wrapper { background: $color-darker; - border-right: 1px solid $color-darker; .sidebar-user { + background: $color-darker; color: $color-light; &:hover { diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb new file mode 100644 index 00000000000..65dbd5ef551 --- /dev/null +++ b/app/controllers/abuse_reports_controller.rb @@ -0,0 +1,24 @@ +class AbuseReportsController < ApplicationController + def new + @abuse_report = AbuseReport.new + @abuse_report.user_id = params[:user_id] + end + + def create + @abuse_report = AbuseReport.new(report_params) + @abuse_report.reporter = current_user + + if @abuse_report.save + message = "Thank you for your report. A GitLab administrator will look into it shortly." + redirect_to root_path, notice: message + else + render :new + end + end + + private + + def report_params + params.require(:abuse_report).permit(:user_id, :message) + end +end diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb new file mode 100644 index 00000000000..34f37bca4ad --- /dev/null +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -0,0 +1,11 @@ +class Admin::AbuseReportsController < Admin::ApplicationController + def index + @abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) + end + + def destroy + AbuseReport.find(params[:id]).destroy + + redirect_to admin_abuse_reports_path, notice: 'Report was removed' + end +end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index f616ccf5684..da5f5bb83fa 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -23,7 +23,8 @@ class Admin::ProjectsController < Admin::ApplicationController end def transfer - ::Projects::TransferService.new(@project, current_user, params.dup).execute + namespace = Namespace.find_by(id: params[:new_namespace_id]) + ::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace) @project.reload redirect_to admin_namespace_project_path(@project.namespace, @project) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 7a683098df3..6092c79c254 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -55,6 +55,20 @@ class Admin::UsersController < Admin::ApplicationController end end + def confirm + if user.confirm! + redirect_to :back, notice: "Successfully confirmed" + else + redirect_to :back, alert: "Error occurred. User was not confirmed" + end + end + + def disable_two_factor + user.disable_two_factor! + redirect_to admin_user_path(user), + notice: 'Two-factor Authentication has been disabled for this user' + end + def create opts = { force_random_password: true, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 63fc146f1d1..3ce8dbc9407 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -183,7 +183,10 @@ class ApplicationController < ActionController::Base headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' - headers['Strict-Transport-Security'] = 'max-age=31536000' if Gitlab.config.gitlab.https + # Enabling HSTS for non-standard ports would send clients to the wrong port + if Gitlab.config.gitlab.https and Gitlab.config.gitlab.port == 443 + headers['Strict-Transport-Security'] = 'max-age=31536000' + end end def add_gon_variables @@ -265,6 +268,7 @@ class ApplicationController < ActionController::Base params[:scope] = 'all' if params[:scope].blank? params[:state] = 'opened' if params[:state].blank? + @sort = params[:sort] @filter_params = params.dup if @project @@ -295,14 +299,14 @@ class ApplicationController < ActionController::Base end def github_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:github) + Gitlab::OAuth::Provider.enabled?(:github) end def gitlab_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:gitlab) + Gitlab::OAuth::Provider.enabled?(:gitlab) end def bitbucket_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 11af9895261..5c3ca8e23c9 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,25 +1,40 @@ class AutocompleteController < ApplicationController + skip_before_action :authenticate_user!, only: [:users] + def users - @users = - if params[:project_id].present? - project = Project.find(params[:project_id]) + begin + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) - if can?(current_user, :read_project, project) - project.team.users - end - elsif params[:group_id] - group = Group.find(params[:group_id]) + if can?(current_user, :read_project, project) + project.team.users + end + elsif params[:group_id] + group = Group.find(params[:group_id]) - if can?(current_user, :read_group, group) - group.users + if can?(current_user, :read_group, group) + group.users + end + elsif current_user + User.all end - else - User.all + rescue ActiveRecord::RecordNotFound + if current_user + return render json: {}, status: 404 end + end + + if @users.nil? && current_user.nil? + authenticate_user! + end + @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? @users = @users.active @users = @users.page(params[:page]).per(PER_PAGE) + # Always include current user if available to filter by "Me" + @users = User.find(@users.pluck(:id) + [current_user.id]).uniq if current_user render json: @users, only: [:name, :username, :id], methods: [:avatar_url] end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 4df9d1b7533..6878d4bc07e 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -18,4 +18,10 @@ class Groups::ApplicationController < ApplicationController return render_404 end end + + def authorize_admin_group_member! + unless can?(current_user, :admin_group_member, group) + return render_403 + end + end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 040255f08e6..91518c44a98 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -5,6 +5,7 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_read_group! before_action :authorize_admin_group!, except: [:index, :leave] + before_action :authorize_admin_group_member!, only: [:create, :resend_invite] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -28,6 +29,9 @@ class Groups::GroupMembersController < Groups::ApplicationController def update @member = @group.group_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_group_member, @member) + @member.update_attributes(member_params) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 901c1cdddcb..279c6ef0f4d 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -24,7 +24,7 @@ class GroupsController < Groups::ApplicationController if @group.save @group.add_owner(current_user) - redirect_to @group, notice: 'Group was successfully created.' + redirect_to @group, notice: "Group '#{@group.name}' was successfully created." else render action: "new" end @@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController def update if @group.update_attributes(group_params) - redirect_to edit_group_path(@group), notice: 'Group was successfully updated.' + redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else render action: "edit" end @@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController def destroy DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, notice: 'Group was removed.' + redirect_to root_path, alert: "Group '#{@group.name} was deleted." end protected diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index ca78a4aaa8e..4e6c0b66634 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -3,6 +3,7 @@ class Import::BitbucketController < Import::BaseController before_action :bitbucket_auth, except: :callback rescue_from OAuth::Error, with: :bitbucket_unauthorized + rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized def callback request_token = session.delete(:oauth_request_token) @@ -21,6 +22,7 @@ class Import::BitbucketController < Import::BaseController def status @repos = client.projects + @incompatible_repos = client.incompatible_projects @already_added_projects = current_user.created_projects.where(import_type: "bitbucket") already_added_projects_names = @already_added_projects.pluck(:import_source) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index fd51b380da2..523264b8ea9 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -72,10 +72,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end rescue Gitlab::OAuth::SignupDisabledError => e - message = "Signing in using your #{oauth['provider']} account without a pre-existing GitLab account is not allowed." + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} 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." + message << " Create a GitLab account first, and then connect it to your #{label} account." end flash[:notice] = message diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 538b09ca54d..f83b4abd1e2 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -32,6 +32,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController params.require(:user).permit( :color_scheme_id, :dashboard, + :project_view, :theme_id ) end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 03845f1e1ec..f9af0871cf1 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -29,13 +29,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def destroy - current_user.update_attributes({ - two_factor_enabled: false, - encrypted_otp_secret: nil, - encrypted_otp_secret_iv: nil, - encrypted_otp_secret_salt: nil, - otp_backup_codes: nil - }) + current_user.disable_two_factor! redirect_to profile_account_path end diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 3362264dcce..45e157c90cb 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -7,7 +7,7 @@ class Projects::BlameController < Projects::ApplicationController before_action :authorize_download_code! def show + @blob = @repository.blob_at(@commit.id, @path) @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) - @blob = @blame.blob end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 100d3d3b317..b762518d377 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,20 +13,27 @@ class Projects::BlobController < Projects::ApplicationController before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] before_action :from_merge_request, only: [:edit, :update] - before_action :require_branch_head, only: [:edit, :update] - before_action :editor_variables, except: [:show, :preview, :diff] before_action :after_edit_path, only: [:edit, :update] + before_action :require_branch_head, only: [:edit, :update] def new commit unless @repository.empty? end def create - result = Files::CreateService.new(@project, current_user, @commit_params).execute + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new( + @project, + current_user, + params.merge(new_branch: sanitized_new_branch_name), + @ref, + file_path + ).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) + ref = sanitized_new_branch_name.presence || @ref + redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(ref, file_path)) else flash[:alert] = result[:message] render :new @@ -41,10 +48,22 @@ class Projects::BlobController < Projects::ApplicationController end def update - result = Files::UpdateService.new(@project, current_user, @commit_params).execute + result = Files::UpdateService. + new( + @project, + current_user, + params.merge(new_branch: sanitized_new_branch_name), + @ref, + @path + ).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" + + if from_merge_request + from_merge_request.reload_code + end + redirect_to after_edit_path else flash[:alert] = result[:message] @@ -61,11 +80,12 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - result = Files::DeleteService.new(@project, current_user, @commit_params).execute + result = Files::DeleteService.new(@project, current_user, params, @ref, @path).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) + redirect_to namespace_project_tree_path(@project.namespace, @project, + @ref) else flash[:alert] = result[:message] render :show @@ -115,6 +135,7 @@ class Projects::BlobController < Projects::ApplicationController @id = params[:id] @ref, @path = extract_ref(@id) + rescue InvalidPathError not_found! end @@ -124,8 +145,8 @@ class Projects::BlobController < Projects::ApplicationController if from_merge_request diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "#file-path-#{hexdigest(@path)}" - elsif @target_branch.present? - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + elsif sanitized_new_branch_name.present? + namespace_project_blob_path(@project.namespace, @project, File.join(sanitized_new_branch_name, @path)) else namespace_project_blob_path(@project.namespace, @project, @id) end @@ -139,25 +160,4 @@ class Projects::BlobController < Projects::ApplicationController def sanitized_new_branch_name @new_branch ||= sanitize(strip_tags(params[:new_branch])) end - - def editor_variables - @current_branch = @ref - @target_branch = (sanitized_new_branch_name || @ref) - - @file_path = - if action_name.to_s == 'create' - File.join(@path, File.basename(params[:file_name])) - else - @path - end - - @commit_params = { - file_path: @file_path, - current_branch: @current_branch, - target_branch: @target_branch, - commit_message: params[:commit_message], - file_content: params[:content], - file_content_encoding: params[:encoding] - } - end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 117ae3aaa3d..3ac0a75fa70 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -17,7 +17,9 @@ class Projects::BranchesController < Projects::ApplicationController def create branch_name = sanitize(strip_tags(params[:branch_name])) + branch_name = Addressable::URI.unescape(branch_name) ref = sanitize(strip_tags(params[:ref])) + ref = Addressable::URI.unescape(ref) result = CreateBranchService.new(project, current_user). execute(branch_name, ref) @@ -32,9 +34,8 @@ class Projects::BranchesController < Projects::ApplicationController end def destroy - status = DeleteBranchService.new(project, current_user).execute(params[:id]) - @branch_name = params[:id] - + @branch_name = Addressable::URI.unescape(params[:id]) + status = DeleteBranchService.new(project, current_user).execute(@branch_name) respond_to do |format| format.html do redirect_to namespace_project_branches_path(@project.namespace, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index bfafdeeb1fb..0f89f2e88cc 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -131,7 +131,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled + return render_404 unless @project.issues_enabled && @project.default_issues_tracker? end # Since iids are implemented only in 6.1 diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 61689488d13..9efe9704d1e 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -64,7 +64,12 @@ class Projects::MilestonesController < Projects::ApplicationController end def destroy - return access_denied! unless can?(current_user, :admin_milestone, @milestone) + return access_denied! unless can?(current_user, :admin_milestone, @project) + + update_params = { milestone: nil } + @milestone.issues.each do |issue| + Issues::UpdateService.new(@project, current_user, update_params).execute(issue) + end @milestone.destroy diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 06aef91cadd..b181c47baec 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -7,6 +7,10 @@ class Projects::NetworkController < Projects::ApplicationController before_action :authorize_download_code! def show + + @url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json)) + @commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s") + respond_to do |format| format.html diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index c4a87e9dbd8..0f5d82ce133 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -30,13 +30,10 @@ class Projects::NotesController < Projects::ApplicationController end def update - if note.editable? - note.update_attributes(note_params) - note.reset_events_cache - end + @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) respond_to do |format| - format.json { render_note_json(note) } + format.json { render_note_json(@note) } format.html { redirect_to :back } end end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index d83561cf32a..6080c849c8d 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,5 +1,6 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath + include TreeHelper before_action :require_non_empty_project before_action :assign_ref_vars @@ -60,6 +61,11 @@ class Projects::RefsController < Projects::ApplicationController } end + if @logs.present? + @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/')) + @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit)) + end + respond_to do |format| format.html { render_404 } format.js diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 1e435be8275..01105532479 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -39,10 +39,13 @@ class Projects::ServicesController < Projects::ApplicationController def test data = Gitlab::PushDataBuilder.build_sample(project, current_user) - if @service.execute(data) + outcome = @service.test(data) + if outcome[:success] message = { notice: 'We sent a request to the provided URL' } else - message = { alert: 'We tried to send a request to the provided URL but an error occured' } + error_message = "We tried to send a request to the provided URL but an error occurred" + error_message << ": #{outcome[:result]}" if outcome[:result].present? + message = { alert: error_message } end redirect_to :back, message diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index b659e15f242..92e4bc16d9d 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -7,13 +7,15 @@ class Projects::TreeController < Projects::ApplicationController before_action :authorize_download_code! def show + return not_found! unless @repository.commit(@ref) + if tree.entries.empty? if @repository.blob_at(@commit.id, @path) redirect_to( namespace_project_blob_path(@project.namespace, @project, File.join(@ref, @path)) ) and return - else + elsif @path.present? return not_found! end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index be85521ffa5..dafc11d0707 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,6 +1,6 @@ class ProjectsController < ApplicationController prepend_before_filter :render_go_import, only: [:show] - skip_before_action :authenticate_user!, only: [:show] + skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] @@ -24,7 +24,7 @@ class ProjectsController < ApplicationController if @project.saved? redirect_to( project_path(@project), - notice: 'Project was successfully created.' + notice: "Project '#{@project.name}' was successfully created." ) else render 'new' @@ -36,11 +36,11 @@ class ProjectsController < ApplicationController respond_to do |format| if status - flash[:notice] = 'Project was successfully updated.' + flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do redirect_to( edit_project_path(@project), - notice: 'Project was successfully updated.' + notice: "Project '#{@project.name}' was successfully updated." ) end format.js @@ -52,10 +52,11 @@ class ProjectsController < ApplicationController end def transfer - transfer_params = params.permit(:new_namespace_id) - ::Projects::TransferService.new(project, current_user, transfer_params).execute - if @project.errors[:namespace_id].present? - flash[:alert] = @project.errors[:namespace_id].first + namespace = Namespace.find_by(id: params[:new_namespace_id]) + ::Projects::TransferService.new(project, current_user).execute(namespace) + + if @project.errors[:new_namespace].present? + flash[:alert] = @project.errors[:new_namespace].first end end @@ -81,7 +82,6 @@ class ProjectsController < ApplicationController if @project.empty_repo? render 'projects/empty' else - @last_push = current_user.recent_push(@project.id) if current_user render :show end else @@ -100,7 +100,7 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute - flash[:alert] = 'Project deleted.' + flash[:alert] = "Project '#{@project.name}' was deleted." if request.referer.include?('/admin') redirect_to admin_namespaces_projects_path diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 89629bc0581..8389f07a3bd 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,27 +2,10 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor prepend_before_action :authenticate_with_two_factor, only: [:create] + prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] def new - redirect_path = - if request.referer.present? && (params['redirect_to_referer'] == 'yes') - referer_uri = URI(request.referer) - if referer_uri.host == Gitlab.config.gitlab.host - referer_uri.path - else - request.fullpath - end - else - request.fullpath - end - - # 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 == new_user_session_path - store_location_for(:redirect, redirect_path) - end - if Gitlab.config.ldap.enabled @ldap_servers = Gitlab::LDAP::Config.servers end @@ -55,6 +38,26 @@ class SessionsController < Devise::SessionsController User.find(session[:otp_user_id]) end end + + def store_redirect_path + redirect_path = + if request.referer.present? && (params['redirect_to_referer'] == 'yes') + referer_uri = URI(request.referer) + if referer_uri.host == Gitlab.config.gitlab.host + referer_uri.path + else + request.fullpath + end + else + request.fullpath + end + + # 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 == new_user_session_path + store_location_for(:redirect, redirect_path) + end + end def authenticate_with_two_factor user = self.resource = find_user @@ -90,7 +93,7 @@ class SessionsController < Devise::SessionsController # Prevent alert from popping up on the first page shown after authentication. flash[:alert] = nil - redirect_to omniauth_authorize_path(:user, provider.to_sym) + redirect_to user_omniauth_authorize_path(provider.to_sym) end def valid_otp_attempt?(user) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2eccc0ee31f..ab89aa2c53a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -10,7 +10,7 @@ # state: 'open' or 'closed' or 'all' # group_id: integer # project_id: integer -# milestone_id: integer +# milestone_title: string # assignee_id: integer # search: string # label_name: string @@ -76,7 +76,7 @@ class IssuableFinder return @milestones if defined?(@milestones) @milestones = - if milestones? && params[:milestone_title] != NONE + if milestones? && params[:milestone_title] != Milestone::None.title Milestone.where(title: params[:milestone_title]) else nil diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0b46db4b1c3..a803b66c502 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -213,6 +213,10 @@ module ApplicationHelper Haml::Helpers.preserve(markdown(file_content)) elsif asciidoc?(file_name) asciidoc(file_content) + elsif plain?(file_name) + content_tag :pre, class: 'plain-readme' do + file_content + end else GitHub::Markup.render(file_name, file_content). force_encoding(file_content.encoding).html_safe @@ -221,6 +225,10 @@ module ApplicationHelper simple_format(file_content) end + def plain?(filename) + Gitlab::MarkupHelper.plain?(filename) + end + def markup?(filename) Gitlab::MarkupHelper.markup?(filename) end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 63c3ff5674d..61d14383945 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -28,7 +28,7 @@ module ApplicationSettingsHelper def restricted_level_checkboxes(help_block_id) Gitlab::VisibilityLevel.options.map do |name, level| checked = restricted_visibility_levels(true).include?(level) - css_class = 'btn btn-primary' + css_class = 'btn' css_class += ' active' if checked checkbox_name = 'application_setting[restricted_visibility_levels][]' diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb new file mode 100644 index 00000000000..0e7a37b4cc6 --- /dev/null +++ b/app/helpers/auth_helper.rb @@ -0,0 +1,50 @@ +module AuthHelper + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze + + def ldap_enabled? + Gitlab.config.ldap.enabled + end + + def provider_has_icon?(name) + PROVIDERS_WITH_ICONS.include?(name.to_s) + end + + def auth_providers + Gitlab::OAuth::Provider.providers + end + + def label_for_provider(name) + Gitlab::OAuth::Provider.label_for(name) + end + + def form_based_provider?(name) + FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } + end + + def form_based_providers + auth_providers.select { |provider| form_based_provider?(provider) } + end + + def button_based_providers + auth_providers.reject { |provider| form_based_provider?(provider) } + end + + def provider_image_tag(provider, size = 64) + label = label_for_provider(provider) + + if provider_has_icon?(provider) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + + image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}") + else + label + end + end + + def auth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + + extend self +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 50df3801703..77d99140c43 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, continue: false) - @formatter ||= Rugments::Formatters::HTML.new( + @formatter ||= Rouge::Formatters::HTMLGitlab.new( nowrap: nowrap, cssclass: 'code highlight', lineanchors: true, @@ -8,11 +8,11 @@ module BlobHelper ) begin - @lexer ||= Rugments::Lexer.guess(filename: blob_name, source: blob_content).new + @lexer ||= Rouge::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 + @lexer = Rouge::Lexers::PlainText + result = @formatter.format(@lexer.lex(blob_content)).html_safe end result diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 128de18bc47..45788ba95ac 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -31,8 +31,8 @@ module EmailsHelper end def color_email_diff(diffcontent) - formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github) - lexer = Rugments::Lexers::Diff.new + formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') + lexer = Rouge::Lexers::Diff raw formatter.format(lexer.lex(diffcontent)) end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index a801d3b10aa..eb3f72a307d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -118,7 +118,7 @@ module GitlabMarkdownHelper # Returns a random markdown tip for use as a textarea placeholder def random_markdown_tip - "Tip: #{MARKDOWN_TIPS.sample}" + MARKDOWN_TIPS.sample end private diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index d4c345fe431..6ddb37cd0dc 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -43,21 +43,6 @@ module IssuesHelper end end - def issue_timestamp(issue) - # Shows the created at time and the updated at time if different - ts = time_ago_with_tooltip(issue.created_at, placement: 'bottom', html_class: 'note_created_ago') - if issue.updated_at != issue.created_at - ts << capture_haml do - haml_tag :span do - haml_concat '·' - haml_concat icon('edit', title: 'edited') - haml_concat time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') - end - end - end - ts.html_safe - end - def bulk_update_milestone_options options_for_select([['None (backlog)', -1]]) + options_from_collection_for_select(project_active_milestones, 'id', diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 45ee4fe4135..f8169b4f288 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -61,4 +61,14 @@ module MergeRequestsHelper } ) end + + def source_branch_with_namespace(merge_request) + if merge_request.for_fork? + namespace = link_to(merge_request.source_project_namespace, + project_path(merge_request.source_project)) + namespace + ":#{merge_request.source_branch}" + else + merge_request.source_branch + end + end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 93e33ebefd8..132a893e532 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -29,6 +29,8 @@ module MilestonesHelper end.active grouped_milestones = Milestones::GroupService.new(milestones).execute + grouped_milestones.unshift(Milestone::None) + options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index dda9b17d61d..5f0c921413a 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -23,21 +23,6 @@ module NotesHelper end end - def note_timestamp(note) - # Shows the created at time and the updated at time if different - ts = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') - if note.updated_at != note.created_at - ts << capture_haml do - haml_tag :span do - haml_concat '·' - haml_concat icon('edit', title: 'edited') - haml_concat time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago') - end - end - end - ts.html_safe - end - def noteable_json(noteable) { id: noteable.id, diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb deleted file mode 100644 index 2fdca13ed40..00000000000 --- a/app/helpers/oauth_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -module OauthHelper - def ldap_enabled? - Gitlab.config.ldap.enabled - end - - def default_providers - [:twitter, :github, :gitlab, :bitbucket, :google_oauth2, :ldap] - end - - def enabled_oauth_providers - Devise.omniauth_providers - end - - def enabled_social_providers - enabled_oauth_providers.select do |name| - [:saml, :twitter, :gitlab, :github, :bitbucket, :google_oauth2].include?(name.to_sym) - end - end - - def additional_providers - enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} - end - - def oauth_image_tag(provider, size = 64) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") - end - - def oauth_active?(provider) - current_user.identities.exists?(provider: provider.to_s) - end - - extend self -end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index bceff4fd52e..ea774e28ecf 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -42,6 +42,13 @@ module PreferencesHelper end end + def project_view_choices + [ + ['Readme (default)', :readme], + ['Activity view', :activity] + ] + end + def user_application_theme theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) theme.css_class @@ -50,4 +57,9 @@ module PreferencesHelper def user_color_scheme_class COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) end + + def prefer_readme? + !current_user || + current_user.project_view == 'readme' + end end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb deleted file mode 100644 index 780c7cd5133..00000000000 --- a/app/helpers/profile_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ProfileHelper - def show_profile_username_tab? - current_user.can_change_username? - end - - def show_profile_social_tab? - enabled_social_providers.any? - end - - def show_profile_remove_tab? - signup_enabled? - end -end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 1bcd9a49527..ab9b068de05 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -21,7 +21,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}) - default_opts = { avatar: true, name: true, size: 16 } + default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -32,7 +32,7 @@ module ProjectsHelper author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] # Build name span tag - author_html << content_tag(:span, sanitize(author.name), class: 'author') if opts[:name] + author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] author_html = author_html.html_safe @@ -92,6 +92,16 @@ module ProjectsHelper end end + def can_change_visibility_level?(project, current_user) + return false unless can?(current_user, :change_visibility_level, project) + + if project.forked? + project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE + else + true + end + end + private def get_project_nav_tabs(project, current_user) @@ -121,8 +131,12 @@ module ProjectsHelper nav_tabs << :snippets end + if can?(current_user, :read_label, project) + nav_tabs << :labels + end + if can?(current_user, :read_milestone, project) - nav_tabs << [:milestones, :labels] + nav_tabs << :milestones end nav_tabs.flatten @@ -170,50 +184,69 @@ module ProjectsHelper end end - def contribution_guide_url(project) - if project && contribution_guide = project.repository.contribution_guide - namespace_project_blob_path( + def add_contribution_guide_path(project) + if project && !project.repository.contribution_guide + namespace_project_new_blob_path( project.namespace, project, - tree_join(project.default_branch, - contribution_guide.name) + project.default_branch, + file_name: "CONTRIBUTING.md", + commit_message: "Add contribution guide" ) end end - def changelog_url(project) - if project && changelog = project.repository.changelog - namespace_project_blob_path( + def add_changelog_path(project) + if project && !project.repository.changelog + namespace_project_new_blob_path( project.namespace, project, - tree_join(project.default_branch, - changelog.name) + project.default_branch, + file_name: "CHANGELOG", + commit_message: "Add changelog" ) end end - def license_url(project) - if project && license = project.repository.license - namespace_project_blob_path( + def add_license_path(project) + if project && !project.repository.license + namespace_project_new_blob_path( project.namespace, project, - tree_join(project.default_branch, - license.name) + project.default_branch, + file_name: "LICENSE", + commit_message: "Add license" ) end end - def version_url(project) - if project && version = project.repository.version + def contribution_guide_path(project) + if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( project.namespace, project, tree_join(project.default_branch, - version.name) + contribution_guide.name) ) end end + def readme_path(project) + filename_path(project, :readme) + end + + def changelog_path(project) + filename_path(project, :changelog) + end + + def license_path(project) + filename_path(project, :license) + end + + def version_path(project) + filename_path(project, :version) + end + def hidden_pass_url(original_url) result = URI(original_url) result.password = '*****' unless result.password.nil? @@ -238,16 +271,6 @@ module ProjectsHelper end end - def service_field_value(type, value) - return value unless type == 'password' - - if value.present? - "***********" - else - nil - end - end - def user_max_access_in_project(user, project) level = project.team.max_member_access(user) @@ -266,4 +289,42 @@ module ProjectsHelper namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') end + + def last_push_event + if current_user + current_user.recent_push(@project.id) + end + end + + def readme_cache_key + sha = @project.commit.try(:sha) || 'nil' + [@project.id, sha, "readme"].join('-') + end + + def round_commit_count(project) + count = project.commit_count + + if count > 10000 + '10000+' + elsif count > 5000 + '5000+' + elsif count > 1000 + '1000+' + else + count + end + end + + private + + def filename_path(project, filename) + if project && blob = project.repository.send(filename) + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + blob.name) + ) + end + end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 00d4c7f1051..b52cd23aba2 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -86,4 +86,10 @@ module VisibilityLevelHelper def default_snippet_visibility current_application_settings.default_snippet_visibility end + + def skip_level?(form_model, level) + form_model.is_a?(Project) && + form_model.forked? && + !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index d3631d49ec6..f8e5afa9b01 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -31,10 +31,11 @@ class Ability end if project && project.public? - [ + rules = [ :read_project, :read_wiki, :read_issue, + :read_label, :read_milestone, :read_project_snippet, :read_project_member, @@ -42,6 +43,8 @@ class Ability :read_note, :download_code ] + + rules - project_disabled_features_rules(project) else group = if subject.kind_of?(Group) subject @@ -102,28 +105,7 @@ 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 + rules - project_disabled_features_rules(project) end end @@ -158,12 +140,13 @@ class Ability :create_project_snippet, :update_issue, :admin_issue, - :admin_label, + :admin_label ] end def project_dev_rules project_report_rules + [ + :admin_merge_request, :create_merge_request, :create_wiki, :push_code @@ -205,6 +188,33 @@ class Ability ] end + def project_disabled_features_rules(project) + rules = [] + + 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 + def group_abilities(user, group) rules = [] @@ -223,7 +233,8 @@ class Ability if group.has_owner?(user) || user.admin? rules.push(*[ :admin_group, - :admin_namespace + :admin_namespace, + :admin_group_member ]) end @@ -285,7 +296,7 @@ class Ability rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:admin_group) + can_manage = group_abilities(user, group).include?(:admin_group_member) if can_manage && (user != target_user) rules << :update_group_member diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb new file mode 100644 index 00000000000..c8c39db11bc --- /dev/null +++ b/app/models/abuse_report.rb @@ -0,0 +1,9 @@ +class AbuseReport < ActiveRecord::Base + belongs_to :reporter, class_name: "User" + belongs_to :user + + validates :reporter, presence: true + validates :user, presence: true + validates :message, presence: true + validates :user_id, uniqueness: { scope: :reporter_id } +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index fee52694099..6d1ad82a262 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -14,13 +14,14 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null -# session_expire_delay :integer default(10080), not null # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text -# user_oauth_applications :bool default(TRUE) +# user_oauth_applications :boolean default(TRUE) # after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null # class ApplicationSetting < ActiveRecord::Base diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 967ffd46db0..0ed0dd98a59 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -1,3 +1,17 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + class AuditEvent < ActiveRecord::Base serialize :details, Hash diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 97846b06d72..40642dc63ba 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -12,6 +12,7 @@ module Issuable included do belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" + belongs_to :updated_by, class_name: "User" belongs_to :milestone has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy @@ -159,6 +160,16 @@ module Issuable end end + # Convert this Issuable class name to a format usable by Ability definitions + # + # Examples: + # + # issuable.class # => MergeRequest + # issuable.to_ability_name # => "merge_request" + def to_ability_name + self.class.to_s.underscore + end + private def filter_superceded_votes(votes, notes) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 56849f28ff0..5b0ae411642 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -79,22 +79,36 @@ module Mentionable end end - # If the mentionable_text field is about to change, locate any *added* references and create cross references for - # them. Invoke from an observer's #before_save implementation. - def notice_added_references(p = project, a = author) - ch = changed_attributes - original, mentionable_changed = "", false - self.class.mentionable_attrs.each do |attr| - if ch[attr] - original << ch[attr] - mentionable_changed = true - end - end + # When a mentionable field is changed, creates cross-reference notes that + # don't already exist + def create_new_cross_references!(p = project, a = author) + changes = detect_mentionable_changes + + return if changes.empty? - # Only proceed if the saved changes actually include a chance to an attr_mentionable field. - return unless mentionable_changed + original_text = changes.collect { |_, vals| vals.first }.join(' ') - preexisting = references(p, self.author, original) + preexisting = references(p, self.author, original_text) create_cross_references!(p, a, preexisting) end + + private + + # Returns a Hash of changed mentionable fields + # + # Preference is given to the `changes` Hash, but falls back to + # `previous_changes` if it's empty (i.e., the changes have already been + # persisted). + # + # See ActiveModel::Dirty. + # + # Returns a Hash. + def detect_mentionable_changes + source = (changes.present? ? changes : previous_changes).dup + + mentionable = self.class.mentionable_attrs + + # Only include changed fields that are mentionable + source.select { |key, val| mentionable.include?(key) } + end end diff --git a/app/models/group.rb b/app/models/group.rb index 051c672cb33..4ff610f8e9d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -56,6 +56,12 @@ class Group < Namespace name end + def avatar_url(size = nil) + if avatar.present? + [gitlab_config.url, avatar.url].join + end + end + def owners @owners ||= group_members.owners.map(&:user) end @@ -70,8 +76,24 @@ class Group < Namespace add_users([user], access_level, current_user) end + def add_guest(user, current_user = nil) + add_user(user, Gitlab::Access::GUEST, current_user) + end + + def add_reporter(user, current_user = nil) + add_user(user, Gitlab::Access::REPORTER, current_user) + end + + def add_developer(user, current_user = nil) + add_user(user, Gitlab::Access::DEVELOPER, current_user) + end + + def add_master(user, current_user = nil) + add_user(user, Gitlab::Access::MASTER, current_user) + end + def add_owner(user, current_user = nil) - self.add_user(user, Gitlab::Access::OWNER, current_user) + add_user(user, Gitlab::Access::OWNER, current_user) end def has_owner?(user) diff --git a/app/models/key.rb b/app/models/key.rb index bbc28678177..406a1257b5d 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -24,6 +24,7 @@ class Key < ActiveRecord::Base validates :title, presence: true, length: { within: 0..255 } validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true + validates :key, format: { without: /\n|\r/, message: 'should be a single line' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true @@ -38,6 +39,11 @@ class Key < ActiveRecord::Base self.key = key.strip unless key.blank? end + def publishable_key + #Removes anything beyond the keytype and key itself + self.key.split[0..1].join(' ') + end + # projects that has this key def projects user.authorized_projects diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 53b3fc10ccb..324d1795ab4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -205,14 +205,7 @@ class MergeRequest < ActiveRecord::Base end def check_if_can_be_merged - can_be_merged = - if for_fork? - Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged? - else - project.repository.can_be_merged?(source_branch, target_branch) - end - - if can_be_merged + if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged? mark_as_mergeable else mark_as_unmergeable @@ -235,6 +228,10 @@ class MergeRequest < ActiveRecord::Base execute(self, commit_message) end + def remove_source_branch? + self.should_remove_source_branch && !self.source_project.root_ref?(self.source_branch) && !self.for_fork? + end + def open? opened? || reopened? end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e0c5fec97b7..d28f3c8d3f9 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -14,6 +14,10 @@ # class Milestone < ActiveRecord::Base + # Represents a "No Milestone" state used for filtering Issues and Merge + # Requests that have no milestone assigned. + None = Struct.new(:title).new('No Milestone') + include InternalId include Sortable diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 03d2ab165ea..30ffacadded 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -114,6 +114,9 @@ class Namespace < ActiveRecord::Base end def move_dir + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(path_was) + if gitlab_shell.mv_namespace(path_was, path) # If repositories moved successfully we need to remove old satellites # and send update instructions to users. diff --git a/app/models/note.rb b/app/models/note.rb index 68b9d433ae0..913a8c00337 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -31,8 +31,9 @@ class Note < ActiveRecord::Base participant :author, :mentioned_users belongs_to :project - belongs_to :noteable, polymorphic: true, touch: true + belongs_to :noteable, polymorphic: true belongs_to :author, class_name: "User" + belongs_to :updated_by, class_name: "User" delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true @@ -356,7 +357,11 @@ class Note < ActiveRecord::Base end def set_references - notice_added_references(project, author) + create_new_cross_references!(project, author) + end + + def system? + read_attribute(:system) end def editable? diff --git a/app/models/project.rb b/app/models/project.rb index b161cbe86b9..3dc1729e812 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # require 'carrierwave/orm/activerecord' @@ -36,7 +37,6 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Rails.application.routes.url_helpers include Referable include Sortable @@ -316,7 +316,7 @@ class Project < ActiveRecord::Base end def web_url - [gitlab_config.url, path_with_namespace].join('/') + Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -433,7 +433,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - [gitlab_config.url, namespace_project_avatar_path(namespace, self)].join + Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -571,7 +571,7 @@ class Project < ActiveRecord::Base end def http_url_to_repo - [gitlab_config.url, '/', path_with_namespace, '.git'].join('') + "#{web_url}.git" end # Check if current branch name is marked as protected in the system @@ -683,6 +683,10 @@ class Project < ActiveRecord::Base update_attribute(:repository_size, repository.size) end + def update_commit_count + update_attribute(:commit_count, repository.commit_count) + end + def forks_count ForkedProjectLink.where(forked_from_project_id: self.id).count end @@ -701,14 +705,14 @@ class Project < ActiveRecord::Base ensure_satellite_exists true else - errors.add(:base, 'Failed to fork repository') + errors.add(:base, 'Failed to fork repository via gitlab-shell') false end else if gitlab_shell.add_repository(path_with_namespace) true else - errors.add(:base, 'Failed to create repository') + errors.add(:base, 'Failed to create repository via gitlab-shell') false end end diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 77d48d4af5e..803402c83ee 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -41,7 +41,7 @@ class CiService < Service # Return string with build status or :error symbol # - # Allowed states: 'success', 'failed', 'running', 'pending' + # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' # # # Ex. diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index bf801ba61ad..27fc19379f1 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -38,7 +38,7 @@ class FlowdockService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' } ] end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index c284e19fe50..5aaa4e85cbc 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -22,8 +22,12 @@ class GitlabCiService < CiService API_PREFIX = "api/v1" prop_accessor :project_url, :token - validates :project_url, presence: true, if: :activated? - validates :token, presence: true, if: :activated? + validates :project_url, + presence: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? + validates :token, + presence: true, + format: { with: /\A([A-Za-z0-9]+)\z/ }, if: :activated? after_save :compose_service_hook, if: :activated? diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 6761f00183e..7a15a861abc 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -60,6 +60,16 @@ class HipchatService < Service gate[room].send('GitLab', message, message_options) end + def test(data) + begin + result = execute(data) + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result } + end + private def gate diff --git a/app/models/repository.rb b/app/models/repository.rb index c767d1051d1..24c32d90051 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -94,18 +94,6 @@ class Repository gitlab_shell.rm_tag(path_with_namespace, tag_name) end - def round_commit_count - if commit_count > 10000 - '10000+' - elsif commit_count > 5000 - '5000+' - elsif commit_count > 1000 - '1000+' - else - commit_count - end - end - def branch_names cache.fetch(:branch_names) { raw_repository.branch_names } end @@ -130,28 +118,29 @@ class Repository cache.fetch(:size) { raw_repository.size } end + def cache_keys + %i(size branch_names tag_names commit_count + readme version contribution_guide changelog license) + end + + def build_cache + cache_keys.each do |key| + unless cache.exist?(key) + send(key) + end + end + end + def expire_cache - %i(size branch_names tag_names commit_count graph_log - readme version contribution_guide changelog license).each do |key| + cache_keys.each do |key| cache.expire(key) end end - def graph_log - cache.fetch(:graph_log) do - commits = raw_repository.log(limit: 6000, skip_merges: true, - ref: root_ref) - - commits.map do |rugged_commit| - commit = Gitlab::Git::Commit.new(rugged_commit) - - { - author_name: commit.author_name, - author_email: commit.author_email, - additions: commit.stats.additions, - deletions: commit.stats.deletions, - } - end + def rebuild_cache + cache_keys.each do |key| + cache.expire(key) + send(key) end end @@ -159,6 +148,10 @@ class Repository @lookup_cache ||= {} end + def expire_branch_names + cache.expire(:branch_names) + end + def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} @@ -375,60 +368,48 @@ 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 - } + def merged_to_root_ref?(branch_name) + branch_commit = commit(branch_name) + root_ref_commit = commit(root_ref) - options[:file] = { - content: content, - path: path - } - - Gitlab::Git::Blob.commit(raw_repository, options) + if branch_commit + rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + else + nil + end 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) + def search_files(query, ref) + offset = 2 + args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref}) + Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end - def user_to_comitter(user) - { - email: user.email, - name: user.name, - time: Time.now - } - end + def parse_search_result(result) + ref = nil + filename = nil + startline = 0 + + result.each_line.each_with_index do |line, index| + if line =~ /^.*:.*:\d+:/ + ref, filename, startline = line.split(':') + startline = startline.to_i - index + break + end + end - def can_be_merged?(source_branch, target_branch) - our_commit = rugged.branches[target_branch].target - their_commit = rugged.branches[source_branch].target + data = "" - if our_commit && their_commit - !rugged.merge_commits(our_commit, their_commit).conflicts? + result.each_line do |line| + data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') end + + OpenStruct.new( + filename: filename, + ref: ref, + startline: startline, + data: data + ) end private diff --git a/app/models/security_event.rb b/app/models/security_event.rb index d131c11cb6c..68c00adad59 100644 --- a/app/models/security_event.rb +++ b/app/models/security_event.rb @@ -1,2 +1,16 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + class SecurityEvent < AuditEvent end diff --git a/app/models/service.rb b/app/models/service.rb index 818a6808db5..dcef2866c3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -87,10 +87,16 @@ class Service < ActiveRecord::Base %w(push tag_push issue merge_request) end - def execute + def execute(data) # implement inside child end + def test(data) + # default implementation + result = execute(data) + { success: result.present?, result: result } + end + def can_test? !project.empty_repo? end diff --git a/app/models/user.rb b/app/models/user.rb index dc84f5141d8..57145cc6b6e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -57,6 +57,7 @@ # otp_backup_codes :text # public_email :string(255) default(""), not null # dashboard :integer default(0) +# project_view :integer default(0) # require 'carrierwave/orm/activerecord' @@ -177,6 +178,10 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum dashboard: [:projects, :stars] + # User's Project preference + # Note: When adding an option, it MUST go on the end of the array. + enum project_view: [:readme, :activity] + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -270,6 +275,10 @@ class User < ActiveRecord::Base value: login.to_s.downcase).first end + def find_by_username!(username) + find_by!('lower(username) = ?', username.downcase) + end + def by_username_or_id(name_or_id) where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first end @@ -322,6 +331,16 @@ class User < ActiveRecord::Base @reset_token end + def disable_two_factor! + update_attributes( + two_factor_enabled: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_backup_codes: nil + ) + end + def namespace_uniq namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) @@ -600,7 +619,7 @@ class User < ActiveRecord::Base end def all_ssh_keys - keys.map(&:key) + keys.map(&:publishable_key) end def temp_oauth_email? diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index f587ee266da..bd245100955 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,34 +1,11 @@ module Files class BaseService < ::BaseService - class ValidationError < StandardError; end + attr_reader :ref, :path - def execute - @current_branch = params[:current_branch] - @target_branch = params[:target_branch] - @commit_message = params[:commit_message] - @file_path = params[:file_path] - @file_content = if params[:file_content_encoding] == 'base64' - Base64.decode64(params[:file_content]) - else - params[:file_content] - end - - # Validate parameters - validate - - # Create new branch if it different from current_branch - if @target_branch != @current_branch - create_target_branch - end - - if sha = commit - after_commit(sha, @target_branch) - success - else - error("Something went wrong. Your changes were not committed") - end - rescue ValidationError => ex - error(ex.message) + def initialize(project, user, params, ref, path = nil) + @project, @current_user, @params = project, user, params.dup + @ref = ref + @path = path end private @@ -36,52 +13,5 @@ module Files def repository project.repository end - - def after_commit(sha, branch) - commit = repository.commit(sha) - full_ref = 'refs/heads/' + branch - old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA - GitPushService.new.execute(project, current_user, old_sha, sha, full_ref) - end - - def current_branch - @current_branch ||= params[:current_branch] - end - - def target_branch - @target_branch ||= params[:target_branch] - end - - def raise_error(message) - raise ValidationError.new(message) - end - - def validate - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) - - unless allowed - raise_error("You are not allowed to push into this branch") - end - - unless project.empty_repo? - unless repository.branch_names.include?(@current_branch) - raise_error("You can only create files if you are on top of a branch") - end - - if @current_branch != @target_branch - if repository.branch_names.include?(@target_branch) - raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") - end - end - end - end - - def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch) - - unless result[:status] == :success - raise_error("Something went wrong when we tried to create #{@target_branch} for you") - end - end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 91d715b2d63..23833aa78ec 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,30 +1,52 @@ require_relative "base_service" module Files - class CreateService < Files::BaseService - def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) - end + class CreateService < BaseService + def execute + allowed = Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) - def validate - super + unless allowed + return error("You are not allowed to create file in this branch") + end - file_name = File.basename(@file_path) + file_name = File.basename(path) + file_path = path unless file_name =~ Gitlab::Regex.file_name_regex - raise_error( + return error( 'Your changes could not be committed, because the file name ' + Gitlab::Regex.file_name_regex_message ) end - unless project.empty_repo? - blob = repository.blob_at_branch(@current_branch, @file_path) + if project.empty_repo? + # everything is ok because repo does not have a commits yet + else + unless repository.branch_names.include?(ref) + return error("You can only create files if you are on top of a branch") + end + + blob = repository.blob_at_branch(ref, file_path) if blob - raise_error("Your changes could not be committed, because file with such name exists") + return error("Your changes could not be committed, because file with such name exists") end end + + + new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path) + created_successfully = new_file_action.commit!( + params[:content], + params[:commit_message], + params[:encoding], + params[:new_branch] + ) + + if created_successfully + success + else + error("Your changes could not be committed, because the file has been changed") + end end end end diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 27c881c3430..1497a0f883b 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,9 +1,36 @@ require_relative "base_service" module Files - class DeleteService < Files::BaseService - def commit - repository.remove_file(current_user, @file_path, @commit_message, @target_branch) + class DeleteService < BaseService + def execute + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + + unless allowed + return error("You are not allowed to push into this branch") + end + + unless repository.branch_names.include?(ref) + return error("You can only create files if you are on top of a branch") + end + + blob = repository.blob_at_branch(ref, path) + + unless blob + 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] + ) + + if deleted_successfully + success + else + error("Your changes could not be committed, because the file has been changed") + end end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a20903c6f02..0724d3ae634 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,9 +1,39 @@ require_relative "base_service" module Files - class UpdateService < Files::BaseService - def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + class UpdateService < BaseService + def execute + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + + unless allowed + return error("You are not allowed to push into this branch") + end + + unless repository.branch_names.include?(ref) + return error("You can only create files if you are on top of a branch") + end + + blob = repository.blob_at_branch(ref, path) + + unless blob + 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], + params[:commit_message], + params[:encoding], + params[:new_branch] + ) + + success + rescue Gitlab::Satellite::CheckoutFailed => ex + error("Your changes could not be committed because ref '#{ref}' could not be checked out", 400) + rescue Gitlab::Satellite::CommitFailed => ex + error("Your changes could not be committed. Maybe there was nothing to commit?", 409) + rescue Gitlab::Satellite::PushFailed => ex + error("Your changes could not be committed. Maybe the file was changed by another process?", 409) end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 6135ae65007..5a2c97b08af 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -21,7 +21,6 @@ class GitPushService project.ensure_satellite_exists project.repository.expire_cache - project.update_repository_size if push_remove_branch?(ref, newrev) @push_commits = [] @@ -61,6 +60,7 @@ class GitPushService EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) + ProjectCacheWorker.perform_async(project.id) end protected @@ -133,8 +133,7 @@ class GitPushService end def is_default_branch?(ref) - Gitlab::Git.branch_ref?(ref) && - (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?) + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch end def commit_user(commit) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 075a6118da2..1cc42b0b0ad 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -2,15 +2,15 @@ class GitTagPushService attr_accessor :project, :user, :push_data def execute(project, user, oldrev, newrev, ref) - @project, @user = project, user + project.repository.expire_cache + @project, @user = project, user @push_data = build_push_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - - project.repository.expire_cache + ProjectCacheWorker.perform_async(project.id) true end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f1ef5ca84fe..15b3825f96a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -27,8 +27,10 @@ class IssuableBaseService < BaseService old_branch, new_branch) end - def filter_params - unless can?(current_user, :admin_issue, project) + def filter_params(issuable_ability_name = :issue) + ability = :"admin_#{issuable_ability_name}" + + unless can?(current_user, ability, project) params.delete(:milestone_id) params.delete(:label_ids) params.delete(:assignee_id) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index c3ca04a4343..770f32de944 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -10,6 +10,10 @@ module Issues private + def filter_params + super(:issue) + end + def execute_hooks(issue, action = 'open') issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3220facaf7c..2fc6ef7f356 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -1,17 +1,11 @@ module Issues class UpdateService < Issues::BaseService def execute(issue) - state = params[:state_event] - - case state + case params.delete(:state_event) when 'reopen' Issues::ReopenService.new(project, current_user, {}).execute(issue) when 'close' Issues::CloseService.new(project, current_user, {}).execute(issue) - when 'task_check' - issue.update_nth_task(params[:task_num].to_i, true) - when 'task_uncheck' - issue.update_nth_task(params[:task_num].to_i, false) end params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE @@ -20,8 +14,7 @@ module Issues filter_params old_labels = issue.labels.to_a - if params.present? && issue.update_attributes(params.except(:state_event, - :task_num)) + if params.present? && issue.update_attributes(params.merge(updated_by: current_user)) issue.reset_events_cache if issue.labels != old_labels @@ -42,7 +35,7 @@ module Issues create_title_change_note(issue, issue.previous_changes['title'].first) end - issue.notice_added_references(issue.project, current_user) + issue.create_new_cross_references!(issue.project, current_user) execute_hooks(issue, 'update') end diff --git a/app/services/merge_requests/auto_merge_service.rb b/app/services/merge_requests/auto_merge_service.rb index df793fc997d..cdedf48b0c0 100644 --- a/app/services/merge_requests/auto_merge_service.rb +++ b/app/services/merge_requests/auto_merge_service.rb @@ -5,20 +5,17 @@ module MergeRequests # mark merge request as merged and execute all hooks and notifications # Called when you do merge via GitLab UI class AutoMergeService < BaseMergeService - attr_reader :merge_request, :commit_message - def execute(merge_request, commit_message) - @commit_message = commit_message - @merge_request = merge_request - merge_request.lock_mr - if merge! + if Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message) merge_request.merge + create_merge_event(merge_request, current_user) create_note(merge_request) notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request, 'merge') + true else merge_request.unlock_mr @@ -29,39 +26,5 @@ module MergeRequests merge_request.mark_as_unmergeable false end - - def merge! - if merge_request.for_fork? - Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message) - else - # Merge local branches using rugged instead of satellites - if sha = commit - after_commit(sha, merge_request.target_branch) - end - end - end - - def commit - committer = repository.user_to_comitter(current_user) - - options = { - message: commit_message, - author: committer, - committer: committer - } - - repository.merge(merge_request.source_branch, merge_request.target_branch, options) - end - - def after_commit(sha, branch) - commit = repository.commit(sha) - full_ref = 'refs/heads/' + branch - old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA - GitPushService.new.execute(project, current_user, old_sha, sha, full_ref) - end - - def repository - project.repository - end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e455fe95791..7b306a8a531 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,5 +20,11 @@ module MergeRequests merge_request.project.execute_services(merge_data, :merge_request_hooks) end end + + private + + def filter_params + super(:merge_request) + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index f6570f52241..25d79e22e39 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,17 +11,11 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) - state = params[:state_event] - - case state + case params.delete(:state_event) when 'reopen' MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request) when 'close' MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request) - when 'task_check' - merge_request.update_nth_task(params[:task_num].to_i, true) - when 'task_uncheck' - merge_request.update_nth_task(params[:task_num].to_i, false) end params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE @@ -30,9 +24,7 @@ module MergeRequests filter_params old_labels = merge_request.labels.to_a - if params.present? && merge_request.update_attributes( - params.except(:state_event, :task_num) - ) + if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user)) merge_request.reset_events_cache if merge_request.labels != old_labels @@ -67,7 +59,7 @@ module MergeRequests merge_request.mark_as_unchecked end - merge_request.notice_added_references(merge_request.project, current_user) + merge_request.create_new_cross_references!(merge_request.project, current_user) execute_hooks(merge_request, 'update') end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index b5611d46257..c22a9333ef6 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -1,22 +1,11 @@ module Notes class UpdateService < BaseService - def execute - note = project.notes.find(params[:note_id]) - note.note = params[:note] - if note.save - notification_service.new_note(note) + def execute(note) + return note unless note.editable? - # Skip system notes, like status changes and cross-references. - unless note.system - event_service.leave_note(note, note.author) + note.update_attributes(params.merge(updated_by: current_user)) - # Create a cross-reference note if this Note contains GFM that - # names an issue, merge request, or commit. - note.references.each do |mentioned| - SystemNoteService.cross_reference(mentioned, note.noteable, note.author) - end - end - end + note.reset_events_cache note end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 312b56eb87b..3735a136365 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -70,12 +70,6 @@ class NotificationService reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email') end - # When we close a merge request we should send next emails: - # - # * merge_request author if their notification level is not Disabled - # * merge_request assignee if their notification level is not Disabled - # * project team members with notification level higher then Participating - # def close_mr(merge_request, current_user) close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email') end @@ -84,26 +78,8 @@ class NotificationService reopen_resource_email(issue, issue.project, current_user, 'issue_status_changed_email', 'reopened') end - # When we merge a merge request we should send next emails: - # - # * merge_request author if their notification level is not Disabled - # * merge_request assignee if their notification level is not Disabled - # * project team members with notification level higher then Participating - # def merge_mr(merge_request, current_user) - 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.delete(current_user) - - recipients.each do |recipient| - mailer.merged_merge_request_email(recipient.id, merge_request.id, current_user.id) - end + close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email') end def reopen_mr(merge_request, current_user) @@ -364,8 +340,7 @@ class NotificationService end def new_resource_email(target, project, method) - recipients = build_recipients(target, project) - recipients.delete(target.author) + recipients = build_recipients(target, project, target.author) recipients.each do |recipient| mailer.send(method, recipient.id, target.id) @@ -373,8 +348,7 @@ class NotificationService end def close_resource_email(target, project, current_user, method) - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, current_user.id) @@ -383,8 +357,7 @@ class NotificationService def reassign_resource_email(target, project, current_user, method) assignee_id_was = previous_record(target, "assignee_id") - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) @@ -392,21 +365,15 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = build_recipients(target, project) - recipients.delete(current_user) + recipients = build_recipients(target, project, current_user) recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id) end end - def build_recipients(target, project) - recipients = - if target.respond_to?(:participants) - target.participants - else - [target.author, target.assignee] - end + def build_recipients(target, project, current_user) + recipients = target.participants(current_user) recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) @@ -415,6 +382,8 @@ class NotificationService recipients = add_subscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target) + recipients.delete(current_user) + recipients end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 011f6f6145e..b35aed005da 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -85,6 +85,8 @@ module Projects @project.create_wiki if @project.wiki_enabled? + @project.build_missing_services + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 489e03bd5ef..f43c0ef70e9 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -11,19 +11,16 @@ module Projects include Gitlab::ShellAdapter class TransferError < StandardError; end - def execute - namespace_id = params[:new_namespace_id] - namespace = Namespace.find_by(id: namespace_id) - - if allowed_transfer?(current_user, project, namespace) - transfer(project, namespace) + def execute(new_namespace) + if allowed_transfer?(current_user, project, new_namespace) + transfer(project, new_namespace) else - project.errors.add(:namespace, 'is invalid') + project.errors.add(:new_namespace, 'is invalid') false end rescue Projects::TransferService::TransferError => ex project.reload - project.errors.add(:namespace_id, ex.message) + project.errors.add(:new_namespace, ex.message) false end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml new file mode 100644 index 00000000000..cffd7684008 --- /dev/null +++ b/app/views/abuse_reports/new.html.haml @@ -0,0 +1,24 @@ +- page_title "Report abuse" +%h3.page-title Report abuse +%p Please use this form to report users who create spam issues, comments or behave inappropriately. +%hr += form_for @abuse_report, html: { class: 'form-horizontal'} do |f| + = f.hidden_field :user_id + - if @abuse_report.errors.any? + .alert.alert-danger + - @abuse_report.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :user_id, class: 'control-label' + .col-sm-10 + - name = "#{@abuse_report.user.name} (@#{@abuse_report.user.username})" + = text_field_tag :user_name, name, class: "form-control", readonly: true + .form-group + = f.label :message, class: 'control-label' + .col-sm-10 + = f.text_area :message, class: "form-control", rows: 2, required: true + .help-block + Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. + + .form-actions + = f.submit "Send report", class: "btn btn-create" diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml new file mode 100644 index 00000000000..4449721ae38 --- /dev/null +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -0,0 +1,23 @@ +- reporter = abuse_report.reporter +- user = abuse_report.user +%tr + %td + - if reporter + = link_to reporter.name, [:admin, reporter] + - else + (removed) + %td + = abuse_report.created_at.to_s(:short) + %td + = abuse_report.message + %td + - if user + = link_to user.name, [:admin, user] + - else + (removed) + %td + - if user + = 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" + = link_to 'Remove user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + %td + = link_to 'Remove report', [:admin, abuse_report], method: :delete, class: "btn btn-xs btn-close" diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml new file mode 100644 index 00000000000..4a25848f156 --- /dev/null +++ b/app/views/admin/abuse_reports/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Abuse Reports" +%h3.page-title Abuse Reports +%hr +- if @abuse_reports.present? + %table.table + %thead + %tr + %th Reported by + %th Reported at + %th Message + %th User + %th + %th + = render @abuse_reports + = paginate @abuse_reports +- else + %h4 There are no abuse reports diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 6bef33c6d7a..b67d2116fa4 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -90,7 +90,7 @@ = 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 :home_page_url, class: 'control-label col-sm-2' + = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2' .col-sm-10 = 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 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 187314872de..296497a4cd4 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -51,21 +51,22 @@ = paginate @projects, param_name: 'projects_page', theme: 'gitlab' .col-md-6 - .panel.panel-default - .panel-heading - Add user(s) to the group: - .panel-body.form-holder - %p.light - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + - if can?(current_user, :admin_group_member, @group) + .panel.panel-default + .panel-heading + Add user(s) to the group: + .panel-body.form-holder + %p.light + Read more about project permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do - %div - = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) - %div.prepend-top-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" - %hr - = button_tag 'Add users to group', class: "btn btn-create" + = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + %div + = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) + %div.prepend-top-10 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" + %hr + = button_tag 'Add users to group', class: "btn btn-create" .panel.panel-default .panel-heading %h3.panel-title @@ -86,7 +87,8 @@ (invited) %span.pull-right.light = member.human_access - = 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 + - if can?(current_user, :destroy_group_member, member) + = 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 .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 0525552ebf8..3a788558226 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -8,7 +8,8 @@ .form-group = f.label :provider, class: 'control-label' .col-sm-10 - = f.select :provider, Gitlab::OAuth::Provider.names, { allow_blank: false }, class: 'form-control' + - values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] } + = f.select :provider, values, { allow_blank: false }, class: 'form-control' .form-group = f.label :extern_uid, "Identifier", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 671c4fbc677..7362d904b94 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -1,6 +1,6 @@ %tr %td - = identity.provider + = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})" %td = identity.extern_uid %td diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b0d31170704..5e40d95d1c5 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -33,6 +33,7 @@ = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = hidden_field_tag "filter", params[:filter] = button_tag class: 'btn btn-primary' do %i.fa.fa-search %hr diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 8c6b8e851c4..a383ea57384 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -43,6 +43,7 @@ %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} - if @user.two_factor_enabled? Enabled + = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication' - else Disabled @@ -104,6 +105,16 @@ .col-md-6 - unless @user == current_user + - unless @user.confirmed? + .panel.panel-info + .panel-heading + Confirm user + .panel-body + - if @user.unconfirmed_email.present? + - email = " (#{@user.unconfirmed_email})" + %p This user has an unconfirmed email address#{email}. You may force a confirmation. + %br + = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - if @user.blocked? .panel.panel-info .panel-heading diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 54a39726771..9f5520603cd 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,5 +1,5 @@ = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" + = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" = f.password_field :password, class: "form-control bottom", placeholder: "Password" - if devise_mapping.rememberable? .remember-me.checkbox diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 6ec741e4882..689cd6ed665 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -6,4 +6,4 @@ %label{for: "remember_me"} = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = button_tag "#{server['label']} Sign in", class: "btn-save btn" + = button_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index f8ba9d80ae8..ecf680e7b23 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,10 +1,8 @@ %p %span.light Sign in with - - providers = additional_providers + - providers = button_based_providers - providers.each do |provider| %span.light - - if default_providers.include?(provider) - = 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), method: :post, class: "btn", "data-no-turbolink" => "true" + - has_icon = provider_has_icon?(provider) + = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index c76574db457..bb5e479697d 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -6,7 +6,7 @@ .login-heading %h3 Sign in .login-body - - if ldap_enabled? + - if form_based_providers.any? %ul.nav.nav-tabs - @ldap_servers.each_with_index do |server, i| %li{class: (:active if i.zero?)} diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index b2e5d11279b..0faab4458e9 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,15 +3,13 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - - if event.created_project? - = cache [event, current_user] do + = cache [event, "v1"] do + = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:'' + - if event.created_project? = render "events/event/created_project", event: event - - else - = cache event do - = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:'' - - if event.push? - = render "events/event/push", event: event - - elsif event.commented? - = render "events/event/note", event: event - - else - = render "events/event/common", event: event + - elsif event.push? + = render "events/event/push", event: event + - elsif event.commented? + = render "events/event/note", event: event + - else + = render "events/event/common", event: event diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index c2577a24982..8cf36c711b4 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -8,8 +8,8 @@ - else = event.project_name -- if current_user == event.author && !event.project.private? && twitter_sharing_enabled? - .event-body +- if !event.project.private? && twitter_sharing_enabled? + .event-body{"data-user-is" => event.author_id} .event-note .md %p diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 34a7c00dc43..8bed5cdb9cc 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -17,7 +17,7 @@ - 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) + - create_mr = 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 @@ -34,10 +34,11 @@ 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 + %span{"data-user-is" => event.author_id, "data-display" => "inline"} + 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 + %li.commits-stat{"data-user-is" => event.author_id} = link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do Create Merge Request diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index d65fb529373..1e8a89e3661 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -9,12 +9,12 @@ .project-info - if project.description.present? - %p.project-description.str-truncated - = project.description + .project-description.str-truncated + = markdown(project.description, pipeline: :description) .repo-info - unless project.empty_repo? - = link_to pluralize(project.repository.round_commit_count, 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) + = link_to pluralize(round_commit_count(project), 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) · = link_to pluralize(project.repository.branch_names.count, 'branch'), namespace_project_branches_path(project.namespace, project) · diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index b460e0ff59e..b5f359279d5 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -6,7 +6,8 @@ %span{class: ("list-item-name" if show_controls)} - if member.user = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' - %strong= user.name + %strong + = link_to user.name, user_path(user) %span.cgray= user.username - if user == current_user %span.label.label-success It's you @@ -24,7 +25,7 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group, @group) + - if show_controls && can?(current_user, :admin_group_member, member) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a70d1ff0697..dba395cc8fa 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -17,7 +17,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - - if current_user && current_user.can?(:admin_group, @group) + - if current_user && current_user.can?(:admin_group_member, @group) .pull-right = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do Add members diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 90a6f5f9d2d..d8af0295b2d 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -14,12 +14,16 @@ :plain job = $("tr#repo_#{@repo_id}") job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") -- else +- elsif @project.persisted? :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('<strong>#{link_to @project.path_with_namespace, [@project.namespace.becomes(Namespace), @project]}</strong>') + target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") +- else + :plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 9d2858e4e72..777eb482714 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -3,11 +3,16 @@ %i.fa.fa-bitbucket Import projects from Bitbucket -%p.light - Select projects you want to import. -%hr -%p - = button_tag 'Import all projects', class: "btn btn-success js-import-all" +- if @repos.any? + %p.light + Select projects you want to import. + %hr + %p + - if @incompatible_repos.any? + = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all" + - else + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + %table.table.import-jobs %thead @@ -41,6 +46,24 @@ = "#{repo["owner"]}/#{repo["slug"]}" %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" + - @incompatible_repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" + %td.import-target + %td.import-actions-job-status + = label_tag "Incompatible Project", nil, class: "label label-danger" + +- if @incompatible_repos.any? + %p + One or more of your Bitbucket projects cannot be imported into GitLab + directly because they use Subversion or Mercurial for version control, + rather than Git. Please convert + = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" + and go through the + = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" + again. + :coffeescript new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index dbc68c39bf1..397649dacf8 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -7,16 +7,33 @@ %title= page_title = favicon_link_tag 'favicon.ico' - = stylesheet_link_tag "application", :media => "all" - = stylesheet_link_tag "print", :media => "print" + + = stylesheet_link_tag "application", media: "all" + = stylesheet_link_tag "print", media: "print" + = javascript_include_tag "application" + = csrf_meta_tags + = include_gon + %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'theme-color', content: '#474D57'} + -# Apple Safari/iOS home screen icons + = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' + = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' + = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120' + = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' + + -# Windows 8 pinned site tile + %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')} + %meta{name: 'msapplication-TileColor', content: '#30353E'} + = yield :meta_tags = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render 'layouts/bootlint' if Rails.env.development? + + = render 'layouts/user_styles' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f17f6fdd91c..96e15783a36 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,6 +1,6 @@ .page-with-sidebar{ class: nav_sidebar_class } = render "layouts/broadcast" - .sidebar-wrapper + .sidebar-wrapper.nicescroll - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml new file mode 100644 index 00000000000..b76b3cb5510 --- /dev/null +++ b/app/views/layouts/_user_styles.html.haml @@ -0,0 +1,24 @@ +:css + [data-user-is] { + display: none !important; + } + + [data-user-is="#{current_user.try(:id)}"] { + display: block !important; + } + + [data-user-is="#{current_user.try(:id)}"][data-display="inline"] { + display: inline !important; + } + + [data-user-is-not] { + display: block !important; + } + + [data-user-is-not][data-display="inline"] { + display: inline !important; + } + + [data-user-is-not="#{current_user.try(:id)}"] { + display: none !important; + } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index a3191593dae..2065be3828a 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -57,6 +57,13 @@ %span Service Templates + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse reports" do + = icon('exclamation-circle fw') + %span + Abuse Reports + %span.count= AbuseReport.count(:all) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do = icon('cogs fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 9d216be151a..695ce68a201 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,9 +1,17 @@ %ul.nav.nav-sidebar + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Dashboard + + %li.separate-item + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do = icon('dashboard fw') %span - Activity + Group - if current_user = nav_link(controller: [:group, :milestones]) do = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 72ada771ca4..8075fe32fbc 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'} do + = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span Back to group diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index de5544268a1..33fd5fcef6c 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,12 @@ %ul.nav.nav-sidebar + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Dashboard + + %li.separate-item + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile', data: {placement: 'right'} do = icon('user fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2012478eba9..d17d1c5fbd4 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,4 +1,19 @@ %ul.nav.nav-sidebar + - if @project.group + = nav_link do + = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Group + - else + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Dashboard + + %li.separate-item + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do = icon('home fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 9c86d3c09b2..857fb199957 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,6 +1,6 @@ %ul.nav.nav-sidebar = nav_link do - = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'} do + = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span Back to project diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index ee1b57278b6..c8662a15adb 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -33,7 +33,7 @@ = yield %div.footer{style: "margin-top: 10px;"} %p - \— + — %br - if @target_url #{link_to "View it on GitLab", @target_url} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 378dfa2dce0..767fe2e0e9a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -59,22 +59,22 @@ %div = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - - if show_profile_social_tab? + - if button_based_providers.any? .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| + - button_based_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 provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true" + + - if auth_active?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do = icon('close') - - if show_profile_username_tab? + - if current_user.can_change_username? .panel.panel-warning.update-username .panel-heading Change Username @@ -94,7 +94,7 @@ %div = f.submit 'Save username', class: "btn btn-warning" - - if show_profile_remove_tab? + - if signup_enabled? .panel.panel-danger.remove-account .panel-heading Remove account diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index d2fad31eca2..3a3e6e1b1c4 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -66,4 +66,4 @@ %td= token.scopes %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else - %p.light You dont have any authorized applications + %p.light You don't have any authorized applications diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index f905417f0e2..b76a5b636ac 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -7,13 +7,12 @@ %li= msg .form-group - = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, class: "form-control" - .form-group = f.label :key, class: 'control-label' .col-sm-10 = f.text_area :key, class: "form-control", rows: 8 - + .form-group + = f.label :title, class: 'control-label' + .col-sm-10= f.text_field :title, class: "form-control" .form-actions = f.submit 'Add key', class: "btn btn-create" diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index aa99280fde6..1134317ee06 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -38,5 +38,13 @@ = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') .col-sm-10 = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + .form-group + = f.label :project_view, class: 'control-label' do + Project view + = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank') + .col-sm-10 + = f.select :project_view, project_view_choices, {}, class: 'form-control' + .help-block + Choose what content you want to see when visit project page .panel-footer = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 37a3952635e..9fdeddfcc7a 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -82,12 +82,12 @@ You can change your avatar here - if Gitlab.config.gravatar.enabled %br - or remove the current avatar to revert to #{link_to "gravatar.com", "http://gravatar.com"} + or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} - else You can upload an avatar here - if Gitlab.config.gravatar.enabled %br - or change it at #{link_to "gravatar.com", "http://gravatar.com"} + or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} %hr %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button %i.fa.fa-paperclip diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 74268c9bde2..92dc58c10d7 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -5,7 +5,7 @@ Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('workflow', 'two_factor_authentication'))}. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. %hr diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml new file mode 100644 index 00000000000..ee02b7f6a6c --- /dev/null +++ b/app/views/projects/_activity.html.haml @@ -0,0 +1,15 @@ += render 'projects/last_push' +.hidden-xs + - if current_user + %ul.nav.nav-pills.event_filter.pull-right + %li + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + %hr +.content_list{:"data-href" => activity_project_path(@project)} += spinner + +:coffeescript + new Activities() diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 95c84c96c41..bec40ec27a5 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -3,6 +3,7 @@ .project-identicon-holder = project_icon(@project, alt: '', class: 'project-avatar avatar s90') .project-home-desc.lead + %h1= @project.name - if @project.description.present? = markdown(@project.description, pipeline: :description) @@ -21,6 +22,9 @@ - if can? current_user, :download_code, @project = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download + = icon('download fw') + Download + + = render 'projects/buttons/dropdown' = render "shared/clone_panel" diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml new file mode 100644 index 00000000000..30622d8a910 --- /dev/null +++ b/app/views/projects/_last_push.html.haml @@ -0,0 +1,14 @@ +- if event = last_push_event + - if show_last_push_widget?(event) + .hidden-xs.center + .slead + %span You pushed to + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do + %strong= event.ref_name + branch + #{time_ago_with_tooltip(event.created_at)} + + %div + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do + Create Merge Request + %hr diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml new file mode 100644 index 00000000000..5038edb95ed --- /dev/null +++ b/app/views/projects/_readme.html.haml @@ -0,0 +1,24 @@ +- if readme = @repository.readme + %article.readme-holder#README + .clearfix + .pull-right + + - if can?(current_user, :push_code, @project) + = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do + %i.fa.fa-pencil + .wiki + = cache(readme_cache_key) do + = render_readme(readme) +- else + %h3.page-title + This project does not have README yet + - if can?(current_user, :push_code, @project) + %p.slead + A + %code README + file contains information about other files in a repository and is commonly + distributed with computer software, forming part of its documentation. + %br + We recommend you to + = link_to "add README", new_readme_path, class: 'underlined-link' + file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index a4e41eeb363..6a41cdbc907 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,7 +2,7 @@ %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } .zen-backdrop - classes << ' js-gfm-input markdown-area' - = f.text_area attr, class: classes, placeholder: random_markdown_tip + = f.text_area attr, class: classes, placeholder: '' = link_to nil, class: 'zen-enter-link', tabindex: '-1' do %i.fa.fa-expand Edit in fullscreen diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 25bd93cae87..65674913bb0 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,13 +1 @@ -.hidden-xs - = render "events/event_last_push", event: @last_push - - - if current_user - %ul.nav.nav-pills.event_filter.pull-right - %li - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss - - = render 'shared/event_filter' - %hr -.content_list -= spinner += render 'projects/activity' diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 8019c7f4569..05d5db5d3fe 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -12,25 +12,31 @@ = render "projects/blob/actions" .file-content.blame.highlight %table - - @blame.each do |commit, lines, since| - - commit = Commit.new(commit, @project) + - current_line = 1 + - @blame.each do |raw_commit, line| %tr %td.blame-commit - %span.commit - = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit_short_id" - - = commit_author_link(commit, avatar: true, size: 16) - - = link_to_gfm truncate(commit.title, length: 20), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "row_title" + .commit + - unless @prev_commit && @prev_commit.sha == raw_commit.sha + - commit = Commit.new(raw_commit, @project) + .commit-row-title + %strong + = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" + .pull-right + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" + + .light + = commit_author_link(commit, avatar: false) + authored + #{time_ago_with_tooltip(commit.committed_date)} + - @prev_commit = raw_commit %td.lines.blame-numbers %pre - - (since...(since + lines.count)).each do |i| - = i - \ + = current_line + - current_line += 1 %td.lines %pre{class: 'code highlight white'} %code :erb - <% lines.each do |line| %> - <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> - <% end %> + <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> + diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 9c3e1703c89..96f188e4aa7 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/diff.html.haml b/app/views/projects/blob/diff.html.haml index 84742608986..f3b01ff3288 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -11,7 +11,7 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= line + %td.line_content.noteable_line= ' ' * @form.indent + line - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 7c2a4fece94..dac984f8c31 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -6,12 +6,11 @@ = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' - - unless @project.empty_repo? - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index a1d464bac59..bd2fc43633c 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,4 +1,7 @@ - page_title @blob.path, @ref + += render 'projects/last_push' + %div.tree-ref-holder = render 'shared/ref_switcher', destination: 'blob', path: @path diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 43412624da6..a693c4b282f 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -5,6 +5,11 @@ %strong.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-info default + - elsif @repository.merged_to_root_ref? branch.name + %span.label.label-primary.has_tooltip(title="Merged into #{@repository.root_ref}") + %i.fa.fa-check + merged + - if @project.protected_branch? branch.name %span.label.label-success %i.fa.fa-lock diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml new file mode 100644 index 00000000000..cade930c8cc --- /dev/null +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -0,0 +1,32 @@ +- if current_user + %span.dropdown + %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + - if can?(current_user, :create_issue, @project) + %li + = link_to url_for_new_issue do + = icon('exclamation-circle fw') + New issue + - if can?(current_user, :create_merge_request, @project) + %li + = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do + = icon('tasks fw') + New merge request + - if can?(current_user, :create_snippet, @project) + %li + = link_to new_namespace_project_snippet_path(@project.namespace, @project) do + = icon('file-text-o fw') + New snippet + - if can?(current_user, :push_code, @project) + %li.divider + %li + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + New branch + %li + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + New tag + + diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f0483c79edc..854c154824d 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,13 +1,13 @@ - if current_user && can?(current_user, :fork_project, @project) - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do - = icon('code-fork') + = icon('code-fork fw') Fork %span.count = @project.forks_count - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do - = icon('code-fork') + = icon('code-fork fw') Fork %span.count = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 664ebd18295..5d7df5ae099 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,6 @@ - if current_user = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do + = icon('star fw') - if current_user.starred?(@project) Unstar - else @@ -15,7 +16,7 @@ - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do - = icon('star') + = icon('star fw') Star %span.count = @project.star_count diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index ed4c601bcdb..977ca423f75 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -1,6 +1,7 @@ - too_big = diff_file.diff_lines.count > Commit::DIFF_SAFE_LINES - if too_big - %a.supp_diff_link Changes suppressed. Click to show + .suppressed-container + %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file{class: "#{'hide' if too_big}"} - last_line = 0 diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index da3d4be84ba..caed0e69dc8 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -3,7 +3,7 @@ Too many changes to show. .pull-right - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-sm btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: :html)), class: "btn btn-sm btn-warning" - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 7ef42ac0f8c..e8e65d87f47 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -29,7 +29,7 @@ .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) - = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project), form_model: @project + = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project .form-group = f.label :tag_list, "Tags", class: 'control-label' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index dfe45a3802d..e577d35d560 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -43,6 +43,8 @@ cd existing_folder git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit git push -u origin master - if can? current_user, :remove_project, @project diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 1b45bb1af0c..b6910c8f796 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -3,43 +3,42 @@ .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" - = cache issue do - .issue-title - %span.issue-title-text - = link_to_gfm issue.title, issue_path(issue), class: "row_title" - .issue-labels - - issue.labels.each do |label| - = link_to_label(label, project: issue.project) - .pull-right.light - - if issue.closed? - %span - CLOSED - - if issue.assignee - = link_to_member(@project, issue.assignee, name: false) - - note_count = issue.notes.user.count - - if note_count > 0 - - %span - %i.fa.fa-comments - = note_count - - else - - %span.issue-no-comments - %i.fa.fa-comments - = 0 - - .issue-info - = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - - if issue.votes_count > 0 - = render 'votes/votes_inline', votable: issue - - if issue.milestone + .issue-title + %span.issue-title-text + = link_to_gfm issue.title, issue_path(issue), class: "row_title" + .issue-labels + - issue.labels.each do |label| + = link_to_label(label, project: issue.project) + .pull-right.light + - if issue.closed? + %span + CLOSED + - if issue.assignee + = link_to_member(@project, issue.assignee, name: false) + - note_count = issue.notes.user.count + - if note_count > 0 %span - %i.fa.fa-clock-o - = issue.milestone.title - - if issue.tasks? - %span.task-status - = issue.task_status + %i.fa.fa-comments + = note_count + - else + + %span.issue-no-comments + %i.fa.fa-comments + = 0 + + .issue-info + = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe + - if issue.votes_count > 0 + = render 'votes/votes_inline', votable: issue + - if issue.milestone + + %span + %i.fa.fa-clock-o + = issue.milestone.title + - if issue.tasks? + %span.task-status + = issue.task_status - .pull-right.issue-updated-at - %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} + .pull-right.issue-updated-at + %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 54d33a5ddd1..e7b14e7582c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -9,7 +9,13 @@ Open Issue ##{@issue.iid} %small.creator - · created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} + · created by #{link_to_member(@project, @issue.author)} + = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') + - if @issue.updated_at != @issue.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') .pull-right - if can?(current_user, :create_issue, @project) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 7fa1ee53f76..c6ebfa281a1 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -6,5 +6,5 @@ = pluralize label.open_issues_count, 'open issue' - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn' - = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' + = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b6d9b135c70..007f6c6a787 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -5,24 +5,15 @@ %hr = render "projects/merge_requests/show/mr_box" %hr - .append-bottom-20 - .slead - %span From - - if @merge_request.for_fork? - %strong.label-branch< - - if @merge_request.source_project - = link_to @merge_request.source_project_namespace, namespace_project_path(@merge_request.source_project.namespace, @merge_request.source_project) - - else - \ #{@merge_request.source_project_namespace} - \:#{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} - - else - %strong.label-branch #{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_branch} - - if @merge_request.open? - .btn-group.btn-group-sm.pull-right + .append-bottom-20.mr-source-target + - if @merge_request.open? + .pull-right + - if @merge_request.source_branch_exists? + = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do + = icon('cloud-download fw') + Check out branch + + %span.dropdown %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } = icon('download') Download as @@ -30,10 +21,20 @@ %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) + .light + %span Request to merge + %span.label-branch #{source_branch_with_namespace(@merge_request)} + %span into + %span.label-branch #{@merge_request.target_branch} = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" + - if @merge_request.open? && @merge_request.can_be_merged? + .light + You can also accept this merge request manually using the + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab @@ -56,11 +57,9 @@ #notes.notes.tab-pane.voting_notes = render "projects/merge_requests/discussion" #commits.commits.tab-pane - - if current_page?(action: 'commits') - = render "projects/merge_requests/show/commits" + - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - - if current_page?(action: 'diffs') - = render "projects/merge_requests/show/diffs" + - # This tab is always loaded via AJAX .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml index 8372afa61b5..9210798f39c 100644 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ b/app/views/projects/merge_requests/branch_from.js.haml @@ -1,2 +1,3 @@ :plain $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); + $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml index f7ede0ded53..32fe2d535f3 100644 --- a/app/views/projects/merge_requests/branch_to.js.haml +++ b/app/views/projects/merge_requests/branch_to.js.haml @@ -1,2 +1,3 @@ :plain $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}"); + $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index e0bc1df97ee..72fbe2e27a7 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,4 +1,5 @@ - page_title "Merge Requests" += render 'projects/last_push' .append-bottom-10 .pull-right = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 22f601ac99e..db1575f899a 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -3,42 +3,45 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 How to merge + %h3 Check out, review and merge locally .modal-body - - if @merge_request.for_fork? - - source_remote = @merge_request.source_project.namespace.nil? ? "source" :@merge_request.source_project.namespace.path - - target_remote = @merge_request.target_project.namespace.nil? ? "target" :@merge_request.target_project.namespace.path - %p - %strong Step 1. - Fetch the code and create a new branch pointing to it - %pre.dark + %p + %strong Step 1. + Fetch and check out the branch for this merge request + %pre.dark + - if @merge_request.for_fork? :preserve git fetch #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch} git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} FETCH_HEAD - %p - %strong Step 2. - Merge the branch and push the changes to GitLab - %pre.dark - :preserve - git checkout #{@merge_request.target_branch} - git merge --no-ff #{@merge_request.source_project_path}-#{@merge_request.source_branch} - git push origin #{@merge_request.target_branch} - - else - %p - %strong Step 1. - Update the repo and checkout the branch we are going to merge - %pre.dark + - else :preserve git fetch origin git checkout -b #{@merge_request.source_branch} origin/#{@merge_request.source_branch} - %p - %strong Step 2. - Merge the branch and push the changes to GitLab - %pre.dark + %p + %strong Step 2. + Review the changes locally + + %p + %strong Step 3. + Merge the branch and fix any conflicts that come up + %pre.dark + - if @merge_request.for_fork? + :preserve + git checkout #{@merge_request.target_branch} + git merge --no-ff #{@merge_request.source_project_path}-#{@merge_request.source_branch} + - else :preserve git checkout #{@merge_request.target_branch} git merge --no-ff #{@merge_request.source_branch} - git push origin #{@merge_request.target_branch} + %p + %strong Step 4. + Push the result of the merge to GitLab + %pre.dark + :preserve + git push origin #{@merge_request.target_branch} + - unless @merge_request.can_be_merged_by?(current_user) + %p + Note that pushing to GitLab requires write access to this repository. :javascript $(function(){ 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 4e8144b4de2..9a1eb36fc88 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,10 +1,16 @@ %h4.page-title .issue-box{ class: issue_box_class(@merge_request) } = @merge_request.state_human_name - = "Merge Request ##{@merge_request.iid}" + Merge Request ##{@merge_request.iid} %small.creator · - created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)} + created by #{link_to_member(@project, @merge_request.author)} + = time_ago_with_tooltip(@merge_request.created_at) + - if @merge_request.updated_at != @merge_request.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') .issue-btn-group.pull-right - if can?(current_user, :update_merge_request, @merge_request) diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml index b5704c502c8..f3cc0e7e8a1 100644 --- a/app/views/projects/merge_requests/widget/_closed.html.haml +++ b/app/views/projects/merge_requests/widget/_closed.html.haml @@ -6,4 +6,7 @@ - if @merge_request.closed_event 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 + %p + = succeed '.' do + The changes were not merged into + %span.label-branch= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 4cc9c652b61..4d4e2f68f61 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,43 +1,28 @@ - if @merge_request.has_ci? .mr-widget-heading - .ci_widget.ci-success{style: "display:none"} - = 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"} - = 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| + - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} - = icon("clock-o") + - if status == :success + - status = "passed" + = icon("check-circle") + - else + = icon("circle") %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" + %span.ci-coverage + = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") - Checking for CI status for #{@merge_request.last_commit_short_sha} + Checking CI status for #{@merge_request.last_commit_short_sha}… .ci_widget.ci-not_found{style: "display:none"} - = icon("times") - %span Can not find commit in the CI server - for #{@merge_request.last_commit_short_sha}. - - - .ci_widget.ci-canceled{style: "display:none"} - = 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" + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. .ci_widget.ci-error{style: "display:none"} - = icon("times") - %span Cannot connect to the CI server. Please check your settings and try again. + = icon("times-circle") + Could not connect to the CI server. Please check your settings and try again. :coffeescript $ -> diff --git a/app/views/projects/merge_requests/widget/_locked.html.haml b/app/views/projects/merge_requests/widget/_locked.html.haml index 13ec278847b..78d0783cba0 100644 --- a/app/views/projects/merge_requests/widget/_locked.html.haml +++ b/app/views/projects/merge_requests/widget/_locked.html.haml @@ -2,7 +2,8 @@ = render 'projects/merge_requests/widget/heading' .mr-widget-body %h4 - Merge in progress... + = icon("spinner spin") + Merge in progress… %p - Merging is in progress. While merging this request is locked and cannot be closed. + This merge request is in the process of being merged, during which time it is locked and cannot be closed. diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index a3b13140810..d22dfa085b8 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -7,23 +7,31 @@ by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} %div - - if @source_branch.blank? - Source branch has been removed + - if !@merge_request.source_branch_exists? + = succeed '.' do + The changes were merged into + %span.label-branch= @merge_request.target_branch + The source branch has been removed. - - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? + - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) .remove_source_branch_widget - %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now + %p + = succeed '.' do + The changes were merged into + %span.label-branch= @merge_request.target_branch + You can remove the source branch now. = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do %i.fa.fa-times Remove Source Branch .remove_source_branch_widget.failed.hide - Failed to remove source branch '#{@merge_request.source_branch}' + %p + Failed to remove source branch '#{@merge_request.source_branch}'. .remove_source_branch_in_progress.hide - %i.fa.fa-spinner.fa-spin - - Removing source branch '#{@merge_request.source_branch}'. Please wait. Page will be automatically reloaded. + %p + = icon('spinner spin') + Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload. :coffeescript $('.remove_source_branch').on 'click', -> diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index bb794912f8f..8c61e819374 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -24,6 +24,6 @@ .mr-widget-footer %span %i.fa.fa-check - Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} + Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do != gfm(issues_sentence(@closes_issues)) diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index f5bacaf280a..df20205de1c 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -8,25 +8,18 @@ .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch - Remove source-branch + Remove source branch .accept-control - = link_to "#", class: "modify-merge-commit-link js-toggle-button", title: "Modify merge commit message" do - %i.fa.fa-edit + = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + = icon('edit') Modify commit message .js-toggle-content.hide.prepend-top-20 = render 'shared/commit_message_container', params: params, text: @merge_request.merge_commit_message, rows: 14, hint: true - %br - .light - 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" - :coffeescript $('.accept-mr-form').on 'ajax:before', -> btn = $('.accept_merge_request') btn.disable() btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress") - diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml index eaf113ee568..ab30fa6b243 100644 --- a/app/views/projects/merge_requests/widget/open/_archived.html.haml +++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml @@ -1,2 +1,4 @@ +%h4 + Project is archived %p - %strong Archived projects do not provide commit access. + This merge request cannot be merged because archived projects cannot be written to. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index e775447cb75..b6b8974297e 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,6 +1,6 @@ %strong - %i.fa.fa-spinner.fa-spin - Checking automatic merge… + = icon("spinner spin") + Checking ability to merge automatically… :coffeescript $ -> diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index d1db5fec43a..e6c089fefb2 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -1,9 +1,10 @@ -- if @merge_request.can_be_merged_by?(current_user) - %h4 - This merge request contains merge conflicts that must be resolved. - You can try it manually on the - %strong - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" -- else - %strong This merge request contains merge conflicts that must be resolved. - Only those with write access to this repository can merge merge requests. +%h4 + = icon("exclamation-triangle") + This merge request contains merge conflicts + +%p + Please resolve these conflicts or + - if @merge_request.can_be_merged_by?(current_user) + #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. + - else + ask someone with write access to this repository to merge this request manually. diff --git a/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml index 423fcd48e25..c9f07629493 100644 --- a/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml +++ b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml @@ -1,14 +1,16 @@ -%h4 - Can't be merged -%p - This merge request can not be accepted because branch - - unless @merge_request.source_branch_exists? - %span.label.label-inverse= @merge_request.source_branch - does not exist in - %span.label.label-info= @merge_request.source_project_path - - else - %span.label.label-inverse= @merge_request.target_branch - does not exist in - %span.label.label-info= @merge_request.target_project_path - %br - %strong Please close this merge request or change branches with existing one +- unless @merge_request.source_branch_exists? + %h4 + = icon("exclamation-triangle") + Source branch + %span.label-branch= source_branch_with_namespace(@merge_request) + does not exist + %p + Please restore the source branch or close this merge request and open a new merge request with a different source branch. +- else + %h4 + = icon("exclamation-triangle") + Target branch + %span.label-branch= @merge_request.target_branch + does not exist + %p + Please restore the target branch or use a different target branch. diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml index 82f6ffd8fcb..a8145558ca8 100644 --- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -1,2 +1,4 @@ -%strong This request can be merged automatically. -Only those with write access to this repository can merge merge requests. +%h4 + Ready to be merged automatically +%p + Ask someone with write access to this repository to merge this request. diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml index 4d526576bc2..35626b624b7 100644 --- a/app/views/projects/merge_requests/widget/open/_nothing.html.haml +++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml @@ -1,8 +1,8 @@ -%h4 Nothing to merge -%p +%h4 + = icon("exclamation-triangle") Nothing to merge from - %span.label-branch #{@merge_request.source_branch} - to - %span.label-branch #{@merge_request.target_branch} - %br - Try to use different branches or push new code. + %span.label-branch= source_branch_with_namespace(@merge_request) + into + %span.label-branch= @merge_request.target_branch +%p + Please push new commits to the source branch or use a different target branch. diff --git a/app/views/projects/merge_requests/widget/open/_reload.html.haml b/app/views/projects/merge_requests/widget/open/_reload.html.haml index 5787f6efea4..acfc31725eb 100644 --- a/app/views/projects/merge_requests/widget/open/_reload.html.haml +++ b/app/views/projects/merge_requests/widget/open/_reload.html.haml @@ -1 +1,6 @@ -This merge request cannot be merged. Try to reload the page. +%h4 + = icon("exclamation-triangle") + This merge request failed to be merged automatically + +%p + Please reload the page to find out the reason. diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml index 4ce3ab31278..0cf16542cc1 100644 --- a/app/views/projects/merge_requests/widget/open/_wip.html.haml +++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml @@ -1,13 +1,5 @@ -- if @merge_request.can_be_merged_by?(current_user) - %h4 - This merge request cannot be accepted because it is marked as Work In Progress. +%h4 + This merge request is currently a Work In Progress - %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 accepted. -- else - %strong This merge request is marked as Work In Progress. - Only those with write access to this repository can merge merge requests. +%p + When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 14a0580f966..2ce5358fa74 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -5,6 +5,10 @@ %i.fa.fa-pencil-square-o Edit = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-sm btn-remove" do + %i.fa.fa-trash-o + Remove + %h4 = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - if milestone.expired? and not milestone.closed? diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 5947498e379..7b1681df336 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -19,6 +19,9 @@ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do + %i.fa.fa-trash-o + Remove %hr - if @milestone.issues.any? && @milestone.can_be_closed? diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index a88cf167511..52b5b8b877e 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -17,9 +17,9 @@ :javascript network_graph = new Network({ - url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', - commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', - ref: '#{@ref}', + url: "#{escape_javascript(@url)}", + commit_url: "#{escape_javascript(@commit_url)}", + ref: "#{escape_javascript(@ref)}", commit_id: '#{@commit.id}' }) new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 5114e63c05f..d25fe68242b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -40,7 +40,7 @@ - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: 'btn' do + = link_to status_import_bitbucket_path, class: 'btn', "data-no-turbolink" => "true" do %i.fa.fa-bitbucket Bitbucket - else @@ -85,7 +85,7 @@ %li The import will time out after 4 minutes. For big repositories, use a clone/push combination. %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}. + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. %hr.prepend-botton-10 diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a663950f031..8f7d2e84c70 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -3,10 +3,7 @@ = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' - - .comment-hints.clearfix - .pull-left Comments are parsed with #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'),{ target: '_blank', tabindex: -1 }} - .pull-right Attach files by dragging & dropping or #{link_to 'selecting them', '#', class: 'markdown-selector', tabindex: -1 }. + = render 'projects/notes/hints' .note-form-actions .buttons diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 3fb044d736e..3be8f44b282 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,12 +8,8 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - - .comment-hints.clearfix - .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/notes/hints' .error-alert .note-form-actions diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml new file mode 100644 index 00000000000..6e7929bdab0 --- /dev/null +++ b/app/views/projects/notes/_hints.html.haml @@ -0,0 +1,9 @@ +.comment-hints.clearfix + .pull-left + = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 + tip: + = random_markdown_tip + .pull-right + = link_to '#', class: 'markdown-selector', tabindex: -1 do + = icon('paperclip') + Attach a file diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5478a887f91..de75d44fc41 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -33,7 +33,14 @@ %span.note-last-update = link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do - = note_timestamp(note) + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') + - if note.updated_at != note.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago') + - if note.updated_by && note.updated_by != note.author + by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - if note.superceded?(@notes) - if note.upvote? @@ -56,10 +63,9 @@ .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} - = cache [note, 'markdown'] do - .note-text - = preserve do - = markdown(note.note, {no_header_anchors: true}) + .note-text + = preserve do + = markdown(note.note, {no_header_anchors: true}) = render 'projects/notes/edit_form', note: note - if note.attachment.url diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index 711aa39101b..0301445b5b2 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -12,18 +12,19 @@ .diff-content %table - note.truncated_diff_lines.each do |line| + - type = line.type - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code } - - if line.type == "match" + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" %td.old_line= "..." %td.new_line= "..." %td.line_content.matched= line.text - else - %td.old_line{class: line.type == "new" ? "new" : "old"} - = raw(line.type == "new" ? " " : line.old_pos) - %td.new_line{class: line.type == "new" ? "new" : "old"} - = raw(line.type == "old" ? " " : line.new_pos) - %td.line_content{class: "noteable_line #{line.type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) + %td.old_line + = raw(type == "new" ? " " : line.old_pos) + %td.new_line + = raw(type == "old" ? " " : line.new_pos) + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text) - if line_code == note.line_code = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 35c15cf3a9e..db7f244d002 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -11,9 +11,11 @@ - if @logs.present? :plain var current_url = location.href.replace(/\/?$/, '/'); - var log_url = '#{namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); + var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/'); + if(current_url == log_url) { - // Load 10 more commit log for each file in tree + // Load more commit logs for each file in tree // if we still on the same page - ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); + var url = "#{escape_javascript(@more_log_url)}"; + ajaxGet(url); } diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 17907a42e3c..ebbd3e477fc 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -6,31 +6,52 @@ = render 'shared/no_ssh' = render 'shared/no_password' +- if prefer_readme? + = render 'projects/last_push' + = render "home_panel" .project-stats %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') + = pluralize(number_with_delimiter(@project.commit_count), 'commit') %li = 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') + + - if !prefer_readme? && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + - if @repository.changelog %li - = link_to changelog_url(@project) do - Changelog + = link_to 'Changelog', changelog_path(@project) + - if @repository.license %li - = link_to license_url(@project) do - License + = link_to 'License', license_path(@project) + - if @repository.contribution_guide %li - = link_to contribution_guide_url(@project) do - Contribution guide + = link_to 'Contribution guide', contribution_guide_path(@project) + + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_changelog_path(@project) do + Add Changelog + - unless @repository.license + %li.missing + = link_to add_license_path(@project) do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_contribution_guide_path(@project) do + Add Contribution guide - if @project.archived? .text-warning.center.prepend-top-20 @@ -40,29 +61,10 @@ %hr %section - - if readme = @repository.readme - %article.readme-holder#README - .clearfix - %small.pull-right - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do - %i.fa.fa-file - = readme.name - .wiki - = render_readme(readme) + - if prefer_readme? + = render 'projects/readme' - else - %h3.page-title - This project does not have README yet - - if can?(current_user, :push_code, @project) - %p.slead - A - %code README - file contains information about other files in a repository and is commonly - distributed with computer software, forming part of its documentation. - %br - We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' - file to the repository and GitLab will render it here instead of this message. - + = render 'projects/activity' - if current_user diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index d304690d162..5048154cb2f 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -49,5 +49,5 @@ :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { - ajaxGet('#{@logs_path}'); + ajaxGet("#{escape_javascript(@logs_path)}"); }); diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 50521264a61..a3a4bd4f752 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,3 +1,2 @@ %span.str-truncated - %span.tree_author= commit_author_link(commit, avatar: true, size: 16) = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 04590f65b27..c9e59428e78 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -2,7 +2,9 @@ = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") - + += render 'projects/last_push' + .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 83cd4c66672..5c4dd7f91ae 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -3,6 +3,10 @@ %h3.page-title = @page.title = render 'main_links' + +.wiki-last-edit-by + Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + - if @page.historical? .warning_message This is an old version of this page. @@ -16,6 +20,6 @@ = render_wiki_content(@page) %hr - .wiki-last-edit-by Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 84e9be82c44..58f58eff54d 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,3 +1,4 @@ +- blob = @project.repository.parse_search_result(blob) .blob-result .file-holder .file-title diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index f9c5810e3d0..c03438eb952 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,3 +1,4 @@ +- wiki_blob = @project.repository.parse_search_result(wiki_blob) .blob-result .file-holder .file-title diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index 30d37dceb30..45ec49280d2 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -1,6 +1,6 @@ - name = field[:name] - title = field[:title] || name.humanize -- value = service_field_value(field[:type], @service.send(name)) +- value = @service.send(name) - type = field[:type] - placeholder = field[:placeholder] - choices = field[:choices] @@ -19,6 +19,6 @@ - elsif type == 'select' = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' - = form.password_field name, placeholder: value, class: 'form-control' + = form.password_field name, value: value, class: 'form-control' - if help %span.help-block= help diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index 02416125a72..ebe2eb0433d 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -1,4 +1,5 @@ - Gitlab::VisibilityLevel.values.each do |level| + - next if skip_level?(form_model, level) .radio - restricted = restricted_visibility_levels.include?(level) = form.label "#{model_method}_#{level}" do diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index 46990895d33..19e8c31975b 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -8,7 +8,7 @@ - else none .issuable-context-selectbox - - if can?(current_user, :admin_issue, @project) + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true) %div.prepend-top-20.clearfix @@ -24,7 +24,7 @@ - else none .issuable-context-selectbox - - if can?(current_user, :admin_issue, @project) + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) = hidden_field_tag :issuable_context = f.submit class: 'btn hide' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index a829782fc4f..0e8da8de723 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -43,11 +43,15 @@ placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) .filter-item.inline.milestone-filter - = select_tag('milestone_title', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') + = select_tag('milestone_title', projects_milestones_options, + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Milestone'}) - if @project .filter-item.inline.labels-filter - = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label') + = select_tag('label_name', project_labels_options(@project), + class: 'select2 trigger-submit', include_blank: true, + data: {placeholder: 'Label'}) .pull-right = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index e434e1b6b98..3489bf3f191 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -16,10 +16,10 @@ %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. + <strong>Work In Progress</strong> merge request to be merged 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. + <strong>Work In Progress</strong> merge request from being merged before it's ready. .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 @@ -38,7 +38,7 @@ .clearfix .error-alert %hr -- if can?(current_user, :admin_issue, @project) +- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .form-group .issue-assignee = f.label :assignee_id, class: 'control-label' do @@ -100,7 +100,7 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions - - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? + - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? %p Please review the %strong #{link_to 'guidelines for contribution', guide_url} diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 43d847831d6..64b7f25ad37 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,6 +18,16 @@ = link_to profile_path, class: 'btn btn-sm' do %i.fa.fa-pencil-square-o Edit Profile settings + - elsif current_user + .pull-right + %span.dropdown + %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} + = icon('exclamation-circle') + %ul.dropdown-menu.dropdown-menu-right + %li + = link_to new_abuse_report_path(user_id: @user.id) do + Report abuse + .username @#{@user.username} .description diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 33d8cc8861b..994b8e8ed38 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -45,7 +45,7 @@ class PostReceive def utf8_encode_changes(changes) changes = changes.dup - + changes.force_encoding("UTF-8") return changes if changes.valid_encoding? diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb new file mode 100644 index 00000000000..55cb6af232e --- /dev/null +++ b/app/workers/project_cache_worker.rb @@ -0,0 +1,15 @@ +class ProjectCacheWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(project_id) + project = Project.find(project_id) + project.update_repository_size + project.update_commit_count + + if project.repository.root_ref + project.repository.build_cache + end + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index e6a50afedb1..94832872d13 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -28,7 +28,7 @@ class RepositoryImportWorker project.import_finish project.save project.satellite.create unless project.satellite.exists? - project.update_repository_size + ProjectCacheWorker.perform_async(project.id) Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' end end diff --git a/config/application.rb b/config/application.rb index 7e899cc3b5b..a96e22211e6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,6 +96,7 @@ module Gitlab end redis_config_hash[:namespace] = 'cache:gitlab' + redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash # This is needed for gitlab-shell diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c32ac2042d0..56770335ddc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -209,20 +209,29 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + # - { name: 'google_oauth2', + # label: 'Google', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} - # - { name: 'github', app_id: 'YOUR_APP_ID', + # - { name: 'twitter', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'github', + # label: 'GitHub', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } - # - { name: 'gitlab', app_id: 'YOUR_APP_ID', + # - { name: 'gitlab', + # label: 'GitLab.com', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } - # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} - # - { name: 'saml', + # - { name: 'bitbucket', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'saml', + # label: 'Our SAML Provider', # 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', @@ -247,6 +256,7 @@ production: &base ## Backup settings backup: path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) + # archive_permissions: 0640 # Permissions for the resulting backup.tar file (default: 0600) # keep_time: 604800 # default: 0 (forever) (in seconds) # upload: # # Fog storage connection settings, see http://fog.io/storage/ . @@ -338,6 +348,8 @@ test: # user: YOUR_USERNAME satellites: path: tmp/tests/gitlab-satellites/ + backup: + path: tmp/tests/backups gitlab_shell: path: tmp/tests/gitlab-shell/ repos_path: tmp/tests/repositories/ diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7b5d488f59e..026c1a5792c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -8,6 +8,15 @@ class Settings < Settingslogic def gitlab_on_standard_port? gitlab.port.to_i == (gitlab.https ? 443 : 80) end + + # get host without www, thanks to http://stackoverflow.com/a/6674363/1233435 + def get_host_without_www(url) + url = URI.encode(url) + uri = URI.parse(url) + uri = URI.parse("http://#{url}") if uri.scheme.nil? + host = uri.host.downcase + host.start_with?('www.') ? host[4..-1] : host + end private @@ -147,6 +156,7 @@ Settings['gravatar'] ||= Settingslogic.new({}) Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil? Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' +Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url']) # # GitLab Shell @@ -170,6 +180,7 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index df73ec1304a..7f73546ac89 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -11,6 +11,7 @@ if Gitlab::LDAP::Config.enabled? end end +OmniAuth.config.full_host = Settings.gitlab['url'] 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? diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index d422acb31d6..6139ddbe6cd 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -6,7 +6,8 @@ Doorkeeper.configure do # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do # Put your resource owner authentication logic here. - # Example implementation: + # Ensure user is redirected to redirect_uri after login + session[:user_return_to] = request.fullpath current_user || redirect_to(new_user_session_url) end diff --git a/config/initializers/redis-store-fix-expiry.rb b/config/initializers/redis-store-fix-expiry.rb deleted file mode 100644 index fce0a135330..00000000000 --- a/config/initializers/redis-store-fix-expiry.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Monkey-patch Redis::Store to make 'setex' and 'expire' work with namespacing - -module Gitlab - class Redis - class Store - module Namespace - # Redis::Store#setex in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def setex(key, expires_in, value, options=nil) - namespace(key) { |key| super(key, expires_in, value) } - end - - # Redis::Store#expire in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def expire(key, expires_in) - namespace(key) { |key| super(key, expires_in) } - end - - private - - # Our new definitions of #setex and #expire above assume that the - # #namespace method exists. Because we cannot be sure of that, we - # re-implement the #namespace method from Redis::Store::Namespace so - # that it is available for all Redis::Store instances, whether they use - # namespacing or not. - # - # Based on lib/redis/store/namespace.rb L49-51 (redis-store 1.1.4) - def namespace(key) - if @namespace - yield interpolate(key) - else - # This Redis::Store instance does not use a namespace so we should - # just pass through the key. - yield key - end - end - end - end - end -end - -Redis::Store.class_eval do - include Gitlab::Redis::Store::Namespace -end diff --git a/config/routes.rb b/config/routes.rb index fd04d7b2f54..1166a4b3eba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,9 @@ Gitlab::Application.routes.draw do end end + # Spam reports + resources :abuse_reports, only: [:new, :create] + # # Import # @@ -159,10 +162,13 @@ Gitlab::Application.routes.draw do put :block put :unblock put :unlock + put :confirm + patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end end + resources :abuse_reports, only: [:index, :destroy] resources :applications resources :groups, constraints: { id: /[^\/]+/ } do @@ -480,7 +486,7 @@ Gitlab::Application.routes.draw do end end - resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do + resources :milestones, constraints: { id: /\d+/ } do member do put :sort_issues put :sort_merge_requests diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb index b25d0dfc701..bba2fc4b186 100644 --- a/db/fixtures/development/01_admin.rb +++ b/db/fixtures/development/01_admin.rb @@ -5,7 +5,7 @@ Gitlab::Seeder.quiet do s.email = 'admin@example.com' s.notification_email = 'admin@example.com' s.username = 'root' - s.password = 'password' + s.password = '5iveL!fe' s.admin = true s.projects_limit = 100 s.confirmed_at = DateTime.now diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 87839770924..8f71198e47f 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -11,9 +11,42 @@ Sidekiq::Testing.inline! do 'https://github.com/twitter/flight.git', 'https://github.com/twitter/typeahead.js.git', 'https://github.com/h5bp/html5-boilerplate.git', + 'https://github.com/google/material-design-lite.git', + 'https://github.com/jlevy/the-art-of-command-line.git', + 'https://github.com/FreeCodeCamp/freecodecamp.git', + 'https://github.com/google/deepdream.git', + 'https://github.com/jtleek/datasharing.git', + 'https://github.com/WebAssembly/design.git', + 'https://github.com/airbnb/javascript.git', + 'https://github.com/tessalt/echo-chamber-js.git', + 'https://github.com/atom/atom.git', + 'https://github.com/ipselon/react-ui-builder.git', + 'https://github.com/mattermost/platform.git', + 'https://github.com/purifycss/purifycss.git', + 'https://github.com/facebook/nuclide.git', + 'https://github.com/wbkd/awesome-d3.git', + 'https://github.com/kilimchoi/engineering-blogs.git', + 'https://github.com/gilbarbara/logos.git', + 'https://github.com/gaearon/redux.git', + 'https://github.com/awslabs/s2n.git', + 'https://github.com/arkency/reactjs_koans.git', + 'https://github.com/twbs/bootstrap.git', + 'https://github.com/chjj/ttystudio.git', + 'https://github.com/DrBoolean/mostly-adequate-guide.git', + 'https://github.com/octocat/Spoon-Knife.git', + 'https://github.com/opencontainers/runc.git', + 'https://github.com/googlesamples/android-topeka.git' ] - project_urls.each_with_index do |url, i| + # You can specify how many projects you need during seed execution + size = if ENV['SIZE'].present? + ENV['SIZE'].to_i + else + 8 + end + + + project_urls.first(size).each_with_index do |url, i| group_path, project_path = url.split('/')[-2..-1] group = Group.find_by(path: group_path) diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 1af8dfc0ef0..1c8740f6ba9 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -1,5 +1,5 @@ if ENV['GITLAB_ROOT_PASSWORD'].blank? - password = 'password' + password = '5iveL!fe' expire_time = Time.now else password = ENV['GITLAB_ROOT_PASSWORD'] diff --git a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb index ffa22e6d5ef..61ff0af41f4 100644 --- a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb +++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb @@ -1,5 +1,7 @@ class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration def change - add_column :application_settings, :session_expire_delay, :integer, default: 10080, null: false + unless column_exists?(:application_settings, :session_expire_delay) + add_column :application_settings, :session_expire_delay, :integer, default: 10080, null: false + end end -end
\ No newline at end of file +end diff --git a/db/migrate/20150713160110_add_project_view_to_users.rb b/db/migrate/20150713160110_add_project_view_to_users.rb new file mode 100644 index 00000000000..fe3d206df89 --- /dev/null +++ b/db/migrate/20150713160110_add_project_view_to_users.rb @@ -0,0 +1,5 @@ +class AddProjectViewToUsers < ActiveRecord::Migration + def change + add_column :users, :project_view, :integer, default: 0 + end +end diff --git a/db/migrate/20150717130904_add_commits_count_to_project.rb b/db/migrate/20150717130904_add_commits_count_to_project.rb new file mode 100644 index 00000000000..9b46daa5933 --- /dev/null +++ b/db/migrate/20150717130904_add_commits_count_to_project.rb @@ -0,0 +1,5 @@ +class AddCommitsCountToProject < ActiveRecord::Migration + def change + add_column :projects, :commit_count, :integer, default: 0 + end +end diff --git a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb new file mode 100644 index 00000000000..78d45c7f96b --- /dev/null +++ b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb @@ -0,0 +1,7 @@ +class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration + def change + add_column :notes, :updated_by_id, :integer + add_column :issues, :updated_by_id, :integer + add_column :merge_requests, :updated_by_id, :integer + end +end diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb new file mode 100644 index 00000000000..e97dc4cf04c --- /dev/null +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -0,0 +1,11 @@ +class CreateAbuseReports < ActiveRecord::Migration + def change + create_table :abuse_reports do |t| + t.integer :reporter_id + t.integer :user_id + t.text :message + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fb0982b10fd..6e919f2883b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150620233230) do +ActiveRecord::Schema.define(version: 20150806104937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "abuse_reports", force: true do |t| + t.integer "reporter_id" + t.integer "user_id" + t.text "message" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "application_settings", force: true do |t| t.integer "default_projects_limit" t.boolean "signup_enabled" @@ -128,12 +136,13 @@ ActiveRecord::Schema.define(version: 20150620233230) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "position", default: 0 + t.integer "position", default: 0 t.string "branch_name" t.text "description" t.integer "milestone_id" t.string "state" t.integer "iid" + t.integer "updated_by_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -230,6 +239,7 @@ ActiveRecord::Schema.define(version: 20150620233230) do t.text "description" t.integer "position", default: 0 t.datetime "locked_at" + t.integer "updated_by_id" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -289,6 +299,7 @@ ActiveRecord::Schema.define(version: 20150620233230) do t.integer "noteable_id" t.boolean "system", default: false, null: false t.text "st_diff" + t.integer "updated_by_id" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -374,6 +385,7 @@ ActiveRecord::Schema.define(version: 20150620233230) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.integer "commit_count", default: 0 end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree @@ -517,6 +529,7 @@ ActiveRecord::Schema.define(version: 20150620233230) do t.text "otp_backup_codes" t.string "public_email", default: "", null: false t.integer "dashboard", default: 0 + t.integer "project_view", default: 0 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 7b0873a9111..bb551fc67f7 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -49,7 +49,8 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "description":"fixed login page css paddings" + "description":"fixed login page css paddings", + "work_in_progress": false } ] ``` @@ -94,7 +95,8 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "description":"fixed login page css paddings" + "description":"fixed login page css paddings", + "work_in_progress": false } ``` @@ -118,6 +120,7 @@ Parameters: "project_id": 4, "title": "Blanditiis beatae suscipit hic assumenda et molestias nisi asperiores repellat et.", "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.", + "work_in_progress": false, "state": "reopened", "created_at": "2015-02-02T19:49:39.159Z", "updated_at": "2015-02-02T20:08:49.959Z", @@ -336,14 +339,6 @@ Parameters: ```json { - "author": { - "id": 1, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "blocked": false, - "created_at": "2012-04-29T08:46:00Z" - }, "note": "text1" } ``` diff --git a/doc/api/notes.md b/doc/api/notes.md index ee2f9fa0eac..c683cb883d4 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -31,7 +31,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:22:45Z" + "created_at": "2013-10-02T09:22:45Z", + "system": true, + "upvote": false, + "downvote": false }, { "id": 305, @@ -45,7 +48,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:56:03Z" + "created_at": "2013-10-02T09:56:03Z", + "system": false, + "upvote": false, + "downvote": false } ] ``` diff --git a/doc/api/users.md b/doc/api/users.md index 5dca77b5c7b..7ba2db248ff 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -397,6 +397,138 @@ Parameters: Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. +## List emails + +Get a list of currently authenticated user's emails. + +``` +GET /user/emails +``` + +```json +[ + { + "id": 1, + "email": "email@example.com" + }, + { + "id": 3, + "email": "email2@example.com" + } +] +``` + +Parameters: + +- **none** + +## List emails for user + +Get a list of a specified user's emails. Available only for admin + +``` +GET /users/:uid/emails +``` + +Parameters: + +- `uid` (required) - id of specified user + +## Single email + +Get a single email. + +``` +GET /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +```json +{ + "id": 1, + "email": "email@example.com" +} +``` + +## Add email + +Creates a new email owned by the currently authenticated user. + +``` +POST /user/emails +``` + +Parameters: + +- `email` (required) - email address + +```json +{ + "id": 4, + "email": "email@example.com" +} +``` + +Will return created email with status `201 Created` on success. If an +error occurs a `400 Bad Request` is returned with a message explaining the error: + +```json +{ + "message": { + "email": [ + "has already been taken" + ] + } +} +``` + +## Add email for user + +Create new email owned by specified user. Available only for admin + +``` +POST /users/:id/emails +``` + +Parameters: + +- `id` (required) - id of specified user +- `email` (required) - email address + +Will return created email with status `201 Created` on success, or `404 Not found` on fail. + +## Delete email for current user + +Deletes email owned by currently authenticated user. +This is an idempotent function and calling it on a email that is already deleted +or not available results in `200 OK`. + +``` +DELETE /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +## Delete email for given user + +Deletes email owned by a specified user. Available only for admin. + +``` +DELETE /users/:uid/emails/:id +``` + +Parameters: + +- `uid` (required) - id of specified user +- `id` (required) - email ID + +Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found. + ## Block user Blocks the specified user. Available only for admin. diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index ee57fdc6590..54c1780c3ab 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -1,6 +1,6 @@ # Use Libravatar service with GitLab -GitLab by default supports [Gravatar](gravatar.com) avatar service. +GitLab by default supports [Gravatar](https://gravatar.com) avatar service. Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is [heavily based on gravatar](http://wiki.libravatar.org/api/). diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 3e4400b544b..b904c70e980 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -2,7 +2,7 @@ Step-by-step guides on the basics of working with Git and GitLab. -* [Start using Git on the commandline](start-using-git.md) +* [Start using Git on the command line](start-using-git.md) * [Create and add your SSH Keys](create-your-ssh-keys.md) @@ -13,3 +13,13 @@ Step-by-step guides on the basics of working with Git and GitLab. * [Create a project](create-project.md) * [Create a group](create-group.md) + +* [Create a branch](create-branch.md) + +* [Fork a project](fork-project.md) + +* [Add a file](add-file.md) + +* [Add an image](add-image.md) + +* [Create a Merge Request](add-merge-request.md) diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md new file mode 100644 index 00000000000..57136ac5c39 --- /dev/null +++ b/doc/gitlab-basics/add-file.md @@ -0,0 +1,31 @@ +# How to add a file + +You can create a file in your [shell](command-line-commands.md) or in GitLab. + +To create a file in GitLab, sign in to GitLab. + +Select a project on the right side of your screen: + + + +It's a good idea to [create a branch](create-branch.md), but it's not necessary. + +Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory: + + + +Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use : + + + +Add all the information that you'd like to include in your file: + + + +Add a commit message based on what you just added and then click on "commit changes": + + + +### Note +Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling +what the directory is about. It's the first document you'll find when you open a directory. diff --git a/doc/gitlab-basics/add-image.md b/doc/gitlab-basics/add-image.md new file mode 100644 index 00000000000..476b48a217c --- /dev/null +++ b/doc/gitlab-basics/add-image.md @@ -0,0 +1,62 @@ +# How to add an image + +The following are the steps to add images to your repository in +GitLab: + +Find the image that you’d like to add. + +In your computer files, find the GitLab project to which you'd like to add the image +(you'll find it as a regular file). Click on every file until you find exactly where you'd +like to add the image. There, paste the image. + +Go to your [shell](command-line-commands.md), and add the following commands: + +Add this command for every directory that you'd like to open: +``` +cd NAME-OF-FILE-YOU'D-LIKE-TO-OPEN +``` + +Create a new branch: +``` +git checkout -b NAME-OF-BRANCH +``` + +Check if your image was correctly added to the directory: +``` +ls +``` + +You should see the name of the image in the list shown. + +Move up the hierarchy through directories: +``` +cd ../ +``` + +Check the status and you should see your image’s name in red: +``` +git status +``` + +Add your changes: +``` +git add NAME-OF-YOUR-IMAGE +``` + +Check the status and you should see your image’s name in green: +``` +git status +``` + +Add the commit: +``` +git commit -m “DESCRIBE COMMIT IN A FEW WORDS” +``` + +Now you can push (send) your changes (in the branch NAME-OF-BRANCH) to GitLab (the git remote named 'origin'): +``` +git push origin NAME-OF-BRANCH +``` + +Your image will be added to your branch in your repository in GitLab. Create a [Merge Request](add-merge-request.md) +to integrate your changes to your project. diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md new file mode 100644 index 00000000000..236b4248ea2 --- /dev/null +++ b/doc/gitlab-basics/add-merge-request.md @@ -0,0 +1,42 @@ +# How to create a merge request + +Merge Requests are useful to integrate separate changes that you've made to a project, on different branches. + +To create a new Merge Request, sign in to GitLab. + +Go to the project where you'd like to merge your changes: + + + +Click on "Merge Requests" on the left side of your screen: + + + +Click on "+ new Merge Request" on the right side of the screen: + + + +Select a source branch or branch: + + + +Click on the "compare branches" button: + + + +Add a title and a description to your Merge Request: + + + +Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button: + + + +Your Merge Request will be ready to be approved and published. + +### Note + +After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen. +You may automatically create a Merge Request from your recently created branch when clicking on this button: + + diff --git a/doc/gitlab-basics/basic-git-commands.md b/doc/gitlab-basics/basic-git-commands.md index ed210ba5420..2b5767dd2d3 100644 --- a/doc/gitlab-basics/basic-git-commands.md +++ b/doc/gitlab-basics/basic-git-commands.md @@ -1,58 +1,58 @@ # Basic Git commands -* Go to the master branch to pull the latest changes from there +### Go to the master branch to pull the latest changes from there ``` git checkout master ``` -* Download the latest changes in the project, so that you work on an up-to-date copy (this is important to do every time you work on a project), while you setup tracking branches +### Download the latest changes in the project +This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches. ``` git pull REMOTE NAME-OF-BRANCH -u ``` (REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch) -* Create a branch (remember that spaces won't be recognized, you need to use a hyphen or underscore) +### Create a branch +Spaces won't be recognized, so you need to use a hyphen or underscore. ``` git checkout -b NAME-OF-BRANCH ``` -* Work on a branch that has already been created +### Work on a branch that has already been created ``` git checkout NAME-OF-BRANCH ``` -* To see the changes you've made (it's important to be aware of what's happening and what's the status of your changes) +### View the changes you've made +It's important to be aware of what's happening and what's the status of your changes. ``` git status ``` -* Add changes to commit (you'll be able to see your changes in red when you type "git status") +### Add changes to commit +You'll see your changes in red when you type "git status". ``` git add CHANGES IN RED git commit -m "DESCRIBE THE INTENTION OF THE COMMIT" ``` -* Send changes to gitlab.com +### Send changes to gitlab.com ``` -git push origin NAME-OF-BRANCH +git push REMOTE NAME-OF-BRANCH ``` -* Throw away all changes in the Git repository, but leave unstaged things +### Delete all changes in the Git repository, but leave unstaged things ``` git checkout . ``` -* Delete all changes in the Git repository, including untracked files +### Delete all changes in the Git repository, including untracked files ``` git clean -f ``` -* Remove all the changes that you don't want to send to gitlab.com -``` -git add NAME-OF-FILE -all -``` - -* Merge created branch with master branch. You need to be in the created branch +### Merge created branch with master branch +You need to be in the created branch. ``` git checkout NAME-OF-BRANCH git merge master diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png Binary files differnew file mode 100644 index 00000000000..457af459bb9 --- /dev/null +++ b/doc/gitlab-basics/basicsimages/button-create-mr.png diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index a596bf20c74..b03cca4029c 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -2,46 +2,47 @@ ## Start working on your project -* In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to [GitLab.com](https://gitlab.com) +In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab. -* When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen +When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen.  -* To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step) +To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step).  ## On the command line -* To clone your project, go to your computer's shell and type the following command +### Clone your project +Go to your computer's shell and type the following command: ``` git clone PASTE HTTPS OR SSH HERE ``` -* A clone of the project will be created in your computer +A clone of the project will be created in your computer. -* Go into a project, directory or file to work in it +### Go into a project, directory or file to work in it ``` cd NAME-OF-PROJECT-OR-FILE ``` -* Go back one directory or file +### Go back one directory or file ``` cd ../ ``` -* To see what’s in the directory that you are in +### View what’s in the directory that you are in ``` ls ``` -* Create a directory +### Create a directory ``` mkdir NAME-OF-YOUR-DIRECTORY ``` -* Create a README.md or file in directory +### Create a README.md or file in directory ``` touch README.md nano README.md @@ -51,22 +52,23 @@ nano README.md #### Press: enter ``` -* Remove a file +### Remove a file ``` rm NAME-OF-FILE ``` -* Remove a directory and all of its contents +### Remove a directory and all of its contents ``` rm -rf NAME-OF-DIRECTORY ``` -* View history in the command line +### View history in the command line ``` history ``` -* Carry out commands for which the account you are using lacks authority. (You will be asked for an administrator’s password) +### Carry out commands for which the account you are using lacks authority +You will be asked for an administrator’s password. ``` sudo ``` diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md new file mode 100644 index 00000000000..7556b0f663e --- /dev/null +++ b/doc/gitlab-basics/create-branch.md @@ -0,0 +1,39 @@ +# How to create a branch + +A branch is an independent line of development. + +New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project. + +To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab. + +To create a new branch in GitLab, sign in and then select a project on the right side of your screen: + + + +Click on "commits" on the menu on the left side of your screen: + + + +Click on the "branches" tab: + + + +Click on the "new branch" button on the right side of the screen: + + + +Fill out the information required: + +1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores) + +1. On the "create from" space, add the the name of the branch you want to branch off from + +1. Click on the button "create branch" + + + +### Note: + +You will be able to find and select the name of your branch in the white box next to a project's name: + + diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md index 8e168395ff7..f80ae62e442 100644 --- a/doc/gitlab-basics/create-group.md +++ b/doc/gitlab-basics/create-group.md @@ -2,7 +2,7 @@ ## Create a group -Your projects in [GitLab.com](https://gitlab.com) can be organized in 2 different ways: +Your projects in GitLab can be organized in 2 different ways: under your own namespace for single projects, such as ´your-name/project-1'; or under groups. If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects. diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index e3963f66010..b545d62549d 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,14 +1,12 @@ # How to create a project in GitLab -## Create a project +To create a new project, sign in to GitLab. -* Sign in to [GitLab.com](https://gitlab.com) - -* Go to your Dashboard and click on "new project" on the right side of your screen +Go to your Dashboard and click on "new project" on the right side of your screen.  -* Fill out the required information +Fill out the required information: 1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores) diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md index cb699588cac..c8a73feb028 100644 --- a/doc/gitlab-basics/create-your-ssh-keys.md +++ b/doc/gitlab-basics/create-your-ssh-keys.md @@ -4,34 +4,34 @@ You need to connect your computer to your GitLab account through SSH Keys. They ## Generate your SSH Key -* Create an account on GitLab. Sign up and check your email for your confirmation link +Create an account on GitLab. Sign up and check your email for your confirmation link. -* After you confirm, go to [GitLab.com](https://about.gitlab.com/) and sign in to your account +After you confirm, go to GitLab and sign in to your account. ## Add your SSH Key -* At the top right corner, click on "profile settings" +At the top right corner, click on "profile settings":  -* On the left side menu click on "SSH Keys" +On the left side menu click on "SSH Keys":  -* Then click on the green button "Add SSH Key" +Then click on the green button "Add SSH Key":  -* There, you should paste the SSH Key that your commandline will generate for you. Below you'll find the steps to generate it +There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it:  -## To generate an SSH Key on your commandline +## To generate an SSH Key on your command line -* Go to your [commandline](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it +Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it. -* Copy the SSH Key that your commandline created and paste it on the "Key" box on the GitLab page. The title will be added automatically +Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically.  -* Now, you'll be able to use Git over SSH, instead of Git over HTTP. +Now, you'll be able to use Git over SSH, instead of Git over HTTP. diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md new file mode 100644 index 00000000000..5f8b81ea919 --- /dev/null +++ b/doc/gitlab-basics/fork-project.md @@ -0,0 +1,19 @@ +# How to fork a project + +A fork is a copy of an original repository that you can put somewhere else +or where you can experiment and apply changes that you can later decide if +publishing or not, without affecting your original project. + +It takes just a few steps to fork a project in GitLab. + +Sign in to GitLab. + +Select a project on the right side of your screen: + + + +Click on the "fork" button on the right side of your screen: + + + +Click on the user or group to where you'd like to add the forked project. diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 21d93ed2e4d..b2ceda025c0 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -1,10 +1,10 @@ -# Start using Git on the commandline +# Start using Git on the command line -If you want to start using a Git and GitLab, make sure that you have created an account on [GitLab.com](https://about.gitlab.com/) +If you want to start using a Git and GitLab, make sure that you have created an account on GitLab. ## Open a shell -* Depending on your operating system, find the shell of your preference. Here are some suggestions +Depending on your operating system, find the shell of your preference. Here are some suggestions. - [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX @@ -14,54 +14,48 @@ If you want to start using a Git and GitLab, make sure that you have created an ## Check if Git has already been installed -* Git is usually preinstalled on Mac and Linux - -* Type the following command and then press enter +Git is usually preinstalled on Mac and Linux. +Type the following command and then press enter: ``` git --version ``` -* You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). -* If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window +If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window. -* After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed +After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed. ## Add your Git username and set your email -* It is important because every Git commit that you create will use this information - -* On your shell, type the following command to add your username +It is important because every Git commit that you create will use this information. +On your shell, type the following command to add your username: ``` git config --global user.name ADD YOUR USERNAME ``` -* Then verify that you have the correct username - +Then verify that you have the correct username: ``` git config --global user.name ``` -* To set your email address, type the following command - +To set your email address, type the following command: ``` git config --global user.email ADD YOUR EMAIL ``` -* To verify that you entered your email correctly, type - +To verify that you entered your email correctly, type: ``` git config --global user.email ``` -* You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project +You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project. ## Check your information -* To view the information that you entered, type - +To view the information that you entered, type: ``` git config --global --list ``` diff --git a/doc/install/installation.md b/doc/install/installation.md index cf58abea4eb..202704088a6 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -195,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-12-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab -**Note:** You can change `7-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -404,7 +404,7 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: root - password + 5iveL!fe **Important Note:** On login you'll be prompted to change the password. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 1efc1f7bddf..a78590d512a 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.0 or 2.1 +GitLab requires Ruby (MRI) 2.1 You will have to use the standard MRI implementation of Ruby. We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab needs several Gems that have native extensions. @@ -93,7 +93,7 @@ To change the Unicorn workers when you have the Omnibus package please see [the ## Database -If you want to run the database separately, the **recommended** database size is **1 MB per user**. +If you want to run the database separately expect a size of about 1 MB per user. ## Redis and Sidekiq @@ -113,4 +113,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o ### Common UI problems with IE -If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled.
\ No newline at end of file +If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. diff --git a/doc/integration/gitlab_actions.png b/doc/integration/gitlab_actions.png Binary files differdeleted file mode 100644 index b08f54d137b..00000000000 --- a/doc/integration/gitlab_actions.png +++ /dev/null diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md deleted file mode 100644 index e35bb8ba693..00000000000 --- a/doc/integration/gitlab_buttons_in_gmail.md +++ /dev/null @@ -1,28 +0,0 @@ -# GitLab buttons in Gmail - -GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). - -If correctly setup, emails that require an action will be marked in Gmail. - - - -To get this functioning, you need to be registered with Google. -[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google) - -To aid the registering with Google, GitLab offers a rake task that will send an email to Google whitelisting email address from your GitLab server. - -To check what would be sent to the Google email address, run the rake task: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production -``` - -**This will not send the email but give you the output of how the mail will look.** - -Copy the output of the rake task to [Google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate". - -If you receive "No errors detected" message from the tester you can send the email using: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production SEND=true -``` diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8e2a602ec35..2010cb9b8a1 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -84,7 +84,7 @@ Existing users can enable OmniAuth for specific providers after the account is c 1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. 1. Go to profile settings (the silhouette icon in the top right corner). 1. Select the "Account" tab. -1. Under "Social Accounts" select the desired OmniAuth provider, such as Twitter. +1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter. 1. The user will be redirected to the provider. Once the user authorized GitLab they will be redirected back to GitLab. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index fe9091ad9a8..1350c8f693c 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -2,9 +2,7 @@ To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter will generate a client ID and secret key for you to use. -1. Sign in to [Twitter Developers](https://dev.twitter.com/) area. - -1. Hover over the avatar in the top right corner and select "My applications." +1. Sign in to [Twitter Application Management](https://apps.twitter.com/). 1. Select "Create new app" @@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - - Agree to the "Rules of the Road." + - Agree to the "Developer Agreement".  1. Select "Create your Twitter application." 1. Select the "Settings" tab. -1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in the Twitter." +1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in with Twitter." 1. Select "Update settings" at the bottom to save changes. -1. Select the "API Keys" tab. +1. Select the "Keys and Access Tokens" tab. 1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration. @@ -78,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
\ No newline at end of file diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 70b7e17795d..7a6a1958445 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. +To add or import a user, you can follow the [project users and members +documentation](doc/workflow/add-user/add-user.md). + ## Project | Action | Guest | Reporter | Developer | Master | Owner | @@ -17,6 +20,7 @@ If a user is a GitLab administrator they receive all permissions. | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | +| Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ | diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md index ce5f1936782..f17bbe8f2aa 100644 --- a/doc/profile/preferences.md +++ b/doc/profile/preferences.md @@ -30,3 +30,9 @@ will be. Setting it to **Starred Projects** will make that Dashboard view the default when signing in or clicking the application logo in the upper left. The default is **Your Projects**. + +### Default Project view + +It allows user to choose what content he or she want to see on project page. + +The default is **Readme**. diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index fb215c8b269..f60ce35d3e2 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -63,5 +63,10 @@ your phone's application or a recovery code to log in. 1. Go to **Account**. 1. Click **Disable Two-factor Authentication**. +## Note to GitLab administrators + +You need to take special care to that 2FA keeps working after +[restoring a GitLab backup](../raketasks/backup_restore.md). + [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://fedorahosted.org/freeotp/ diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 770b7a70fe0..a8dc5c24df2 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -7,3 +7,4 @@ - [User management](user_management.md) - [Web hooks](web_hooks.md) - [Import](import.md) of git repositories in bulk +- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
\ No newline at end of file diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 39a13b14fba..6a68c8e8286 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -7,7 +7,14 @@ A backup creates an archive file that contains the database, all repositories and all attachments. 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. +You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore. + +You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json` +(for omnibus packages) or `/home/git/gitlab/.secret` (for installations +from source). This file contains the database encryption key used +for two-factor authentication. If you restore a GitLab backup without +restoring the database encryption key, users who have two-factor +authentication enabled will loose access to your GitLab server. 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)* @@ -141,17 +148,58 @@ with the name of your bucket: } ``` +## Backup archive permissions + +The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. +This is meant to avoid other system users reading GitLab's data. +If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. + +``` +# In /etc/gitlab/gitlab.rb, for omnibus packages +gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable +``` + +``` +# In gitlab.yml, for installations from source: + backup: + archive_permissions: 0644 # Makes the backup archives world-readable +``` + ## Storing configuration files -Please be informed that a backup does not store your configuration files. +Please be informed that a backup does not store your configuration +files. One reason for this is that your database contains encrypted +information for two-factor authentication. Storing encrypted +information along with its key in the same place defeats the purpose +of using encryption in the first place! + If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have an installation from source, please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +If you have an installation from source, please consider backing up your `.secret` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json` +(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your +database encryption key. ## Restore a previously created backup You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. +### Prerequisites + +You need to have a working GitLab installation before you can perform +a restore. This is mainly because the system user performing the +restore actions ('git') is usually not allowed to create or delete +the SQL database it needs to import data into ('gitlabhq_production'). +All existing data will be either erased (SQL) or moved to a separate +directory (repositories, uploads). + +If some or all of your GitLab users are using two-factor authentication +(2FA) then you must also make sure to restore +`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret` +(installations from source). Note that you need to run `gitlab-ctl +reconfigure` after changing `gitlab-secrets.json`. + ### Installation from source ``` @@ -201,7 +249,7 @@ Deleting tmp directories...[DONE] 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`. +First make sure your backup tar file is in `/var/opt/gitlab/backups` (or wherever `gitlab_rails['backup_path']` points to). ```shell sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 3bc92187218..12d6a84b68e 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -6,6 +6,7 @@ It starts 7 working days before the release. The release manager doesn't have to perform all the work but must ensure someone is assigned. The current release manager must schedule the appointment of the next release manager. The new release manager should create overall issue to track the progress. +The release manager should be the only person pushing/merging commits to the x-y-stable branches. ## Release Manager @@ -67,7 +68,7 @@ Xth: (2 working days before the 22nd) Xth: (1 working day before the 22nd) - [ ] Merge CE stable into EE stable -- [ ] Create (hopefully final) CE, EE, CI release candidates (#LINK) +- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow) - [ ] Create Omnibus tags and build packages for the latest release candidates - [ ] Update GitLab.com with the latest RC (#LINK) - [ ] Update ci.gitLab.com with the latest RC (#LINK) @@ -80,10 +81,11 @@ workday to quickly fix any issues. - [ ] Merge CE stable into EE stable (#LINK) - [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK) -- [ ] BEFORE 11AM CET Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) -- [ ] BEFORE 12AM CET Publish the release blog post (#LINK) +- [ ] Create the 'x.y.0' version on version.gitlab.com +- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) +- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK) - [ ] Tweet about the release (blog post) (#LINK) -- [ ] Schedule a second tweet of the release announcement at 6PM CET / 9AM PST +- [ ] Schedule a second tweet of the release announcement with the same text at 6PM CET / 9AM PST ``` @@ -154,6 +156,7 @@ Tweet about the RC release: 1. Also check the CI changelog 1. Add a proposed tweet text to the blog post WIP MR description. 1. Create a WIP MR for the blog post +1. Make sure merge request title starts with `WIP` so it can not be accidently merged until ready. 1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. 1. Decide with core team who will be the MVP user. 1. Create WIP MR for adding MVP to MVP page on website @@ -219,4 +222,4 @@ Consider creating a post on Hacker News. ## Create a WIP blogpost for the next release -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md).
\ No newline at end of file diff --git a/doc/release/patch.md b/doc/release/patch.md index a569bb3da8d..6aa11b283df 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -52,5 +52,6 @@ bundle exec rake release["x.x.x"] 1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) +1. Create the 'x.y.0' version on version.gitlab.com 1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) 1. Create a new patch release issue for the next potential release
\ No newline at end of file diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 5f44f9351dd..7cdcd11c04c 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -105,3 +105,6 @@ IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. + +Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll +have when pushing code via SSH. That's why it needs to uniquely map to a single user. diff --git a/doc/update/6.x-or-7.x-to-7.12.md b/doc/update/6.x-or-7.x-to-7.14.md index 5705fb360db..a1488474f60 100644 --- a/doc/update/6.x-or-7.x-to-7.12.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.12 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.12.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.14 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.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.12. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14. ## 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-12-stable +sudo -u git -H git checkout 7-14-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-12-stable-ee +sudo -u git -H git checkout 7-14-stable-ee ``` ## 4. Install additional packages @@ -127,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.3 +sudo -u git -H git checkout v2.6.4 ``` ## 7. Install libs, migrations, etc. @@ -162,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-12-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7.14-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-12-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-12-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-14-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-14-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 @@ -182,14 +182,14 @@ 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-12-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-12-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-14-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-14-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. ### Check the version of /usr/local/bin/git If you installed Git from source into /usr/local/bin/git then please [check -your version](7.11-to-7.12.md). +your version](7.13-to-7.14.md). ## 9. Start application diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md new file mode 100644 index 00000000000..57ebe3261b6 --- /dev/null +++ b/doc/update/7.12-to-7.13.md @@ -0,0 +1,129 @@ +# From 7.12 to 7.13 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. 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-13-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-13-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.3 +``` + +### 5. 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 +``` + +### 6. 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-12-stable:config/gitlab.yml.example origin/7-13-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. 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.12) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.11 to 7.12](7.11-to-7.12.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/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md new file mode 100644 index 00000000000..7c2d3f4498a --- /dev/null +++ b/doc/update/7.13-to-7.14.md @@ -0,0 +1,129 @@ +# From 7.13 to 7.14 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. 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-14-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-14-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.4 +``` + +### 5. 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 +``` + +### 6. 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-13-stable:config/gitlab.yml.example origin/7-14-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. 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.13) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.12 to 7.13](7.12-to-7.13.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 8ef3e0d55cc..a596ea38456 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,5 +1,5 @@ # 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.* +*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) 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, 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. @@ -16,6 +16,7 @@ git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab cd mysql-postgresql-converter 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 +ed -s gitlabhq_production.psql < move_drop_indexes.ed # Import the database dump as the application database user sudo -u git psql -f gitlabhq_production.psql -d gitlabhq_production @@ -56,6 +57,7 @@ sudo -u git -H git clone https://github.com/gitlabhq/mysql-postgresql-converter. # Convert gitlabhq_production.mysql sudo -u git -H mkdir db sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql +sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed # Compress database backup sudo -u git -H gzip db/database.sql @@ -68,5 +70,5 @@ sudo -u git -H gzip db/database.sql sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz # Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab -# installation. Remember to recreate the indexes after the import. +# installation. ``` diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index e29ee2a7b3d..22b9be059d6 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -22,6 +22,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ```bash cd /home/git/gitlab sudo -u git -H git fetch --all +sudo -u git -H git checkout -- Gemfile.lock db/schema.rb sudo -u git -H git checkout LATEST_TAG ``` diff --git a/doc/workflow/README.md b/doc/workflow/README.md index f1959d30139..3915198ad2a 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -7,9 +7,10 @@ - [Groups](groups.md) - [Keyboard shortcuts](shortcuts.md) - [Labels](labels.md) -- [Notifications](notifications.md) +- [Notification emails](notifications.md) - [Project Features](project_features.md) - [Project forking workflow](forking_workflow.md) +- [Project users](add-user/add-user.md) - [Protected branches](protected_branches.md) - [Web Editor](web_editor.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md new file mode 100644 index 00000000000..8c9b4f72631 --- /dev/null +++ b/doc/workflow/add-user/add-user.md @@ -0,0 +1,25 @@ +# Project users + +You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project. + +Here's how to add or import users to your projects. + +You should have 'master' or 'owner' permissions to add or import a new user +to your project. + +To add or import a user, go to your project and click on "Members" on the left side of your screen: + + + +Select "Add members" or "Import members" on the right side of your screen: + + + +If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to +give the user: + + + +If you are importing a user, follow the steps to select the project where you'd like to import the user from: + + diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png Binary files differnew file mode 100644 index 00000000000..2805c5764a5 --- /dev/null +++ b/doc/workflow/add-user/images/add-members.png diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/images/members.png Binary files differnew file mode 100644 index 00000000000..f1797b95f67 --- /dev/null +++ b/doc/workflow/add-user/images/members.png diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png Binary files differnew file mode 100644 index 00000000000..d500daea56e --- /dev/null +++ b/doc/workflow/add-user/images/new-member.png diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png Binary files differnew file mode 100644 index 00000000000..dd3844edff8 --- /dev/null +++ b/doc/workflow/add-user/images/select-project.png diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 0e87dc74217..f608674faf6 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -31,7 +31,7 @@ We think there is still room for improvement and will detail a set of practices ## Git flow and its problems -[ + Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. @@ -54,7 +54,7 @@ And doing releases doesn't automatically mean also doing hotfixes.  - In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). +In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). This flow has only feature branches and a master branch. This is very simple and clean, many organizations have adopted it with great success. Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches. @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -216,7 +216,7 @@ This prevents creating a merge commit when merging master into your feature bran However, just like with squashing you should never rebase commits you have pushed to a remote server. This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). -You can reuse recorded resolutions (rerere) sometimes, but with without rebasing you only have to solve the conflicts one time and you’re set. +You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set. There has to be a better way to avoid many merge commits. The way to prevent creating many merge commits is to not frequently merge master into the feature branch. diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 19395657719..cd98d1b9852 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -6,4 +6,7 @@ 4. [SVN](migrating_from_svn.md)
### Note
-* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported.
\ No newline at end of file +* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported.
+
+* Repositories are imported to GitLab via HTTP.
+If the repository is too large, it can timeout. We have a soft limit of 10GB.
diff --git a/doc/workflow/labels.md b/doc/workflow/labels.md index 085b7baf5ce..6e4840ca5ae 100644 --- a/doc/workflow/labels.md +++ b/doc/workflow/labels.md @@ -1,6 +1,6 @@ # Labels -In GitLab, you can easily tag issues and merge requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. +In GitLab, you can easily tag issues and Merge Requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. Here you can create a new label. @@ -14,3 +14,5 @@ If you want to change an existing label, press edit next to the listed label. You will be presented with the same form as when creating a new label.  + +You can add labels to Merge Requests when you create or edit them. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 17215de677e..80817c98d22 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -1,6 +1,6 @@ -# GitLab Notifications +# GitLab Notification Emails -GitLab has notifications system in place to notify a user of events important for the workflow. +GitLab has a notification system in place to notify a user of events that are important for the workflow. ## Notification settings @@ -52,20 +52,40 @@ Below is the table of events users can be notified of: | New SSH key added | User | Security email, always sent. | | New email added | User | Security email, always sent. | | New user created | User | Sent on user creation, except for omniauth (LDAP)| -| New issue created | Issue assignee [1], project members [2] | [1] not disabled, [2] higher than participating | | User added to project | User | Sent when user is added to project | | Project access level changed | User | Sent when user project access level is changed | | User added to group | User | Sent when user is added to group | +| Group access level changed | User | Sent when user group access level is changed | | Project moved | Project members [1] | [1] not disabled | -| Group access level changed | User | Sent when user group access level is changed | -| Close issue | Issue author [1], issue assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| Reassign issue | New issue assignee [1], old issue assignee [2] | [1] [2] not disabled | -| Reopen issue | Project members [1] | [1] higher than participating | -| New merge request | MR assignee [1] | [1] not disabled | -| Reassign merge request | New MR assignee [1], old MR assignee [2] | [1] [2] not disabled | -| Close merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| Reopen merge request | Project members [1] | [1] higher than participating | -| Merge merge request | MR author [1], MR assignee [2], project members [3] | [1] [2] not disabled, [3] higher than participating | -| New comment | Mentioned users [1], users participating [2], project members [3] | [1] [2] not disabled, [3] higher than participating | - +### Issue / Merge Request events + +In all of the below cases, the notification will be sent to: +- Participants: + - the author and assignee of the issue/merge request + - authors of comments on the issue/merge request + - anyone mentioned by `@username` in the issue/merge request description + - anyone mentioned by `@username` in any of the comments on the issue/merge request + + ...with notification level "Participating" or higher + +- Watchers: project members with notification level "Watch" +- Subscribers: anyone who manually subscribed to the issue/merge request + +| Event | Sent to | +|------------------------|---------| +| New issue | | +| Close issue | | +| Reassign issue | The above, plus the old assignee | +| Reopen issue | | +| New merge request | | +| Reassign merge request | The above, plus the old assignee | +| Close merge request | | +| Reopen merge request | | +| Merge merge request | | +| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | + +You won't receive notifications for Issues, Merge Requests or Milestones +created by yourself. You will only receive automatic notifications when +somebody else comments or adds changes to the ones that you've created or +mentions you. diff --git a/docker/single/Dockerfile b/docker/Dockerfile index a6cbf131237..05521af6963 100644 --- a/docker/single/Dockerfile +++ b/docker/Dockerfile @@ -7,7 +7,9 @@ RUN apt-get update -q \ ca-certificates \ openssh-server \ wget \ - apt-transport-https + apt-transport-https \ + vim \ + nano # Download & Install GitLab # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. @@ -23,12 +25,23 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ && mkdir -p /var/run/sshd +# Prepare default configuration +RUN ( \ + echo "" && \ + echo "# Docker options" && \ + echo "# Prevent Postgres from trying to allocate 25% of total memory" && \ + echo "postgresql['shared_buffers'] = '1MB'" ) >> /etc/gitlab/gitlab.rb && \ + mkdir -p /assets/ && \ + cp /etc/gitlab/gitlab.rb /assets/gitlab.rb + # Expose web & ssh -EXPOSE 80 22 +EXPOSE 443 80 22 + +# Define data volumes +VOLUME ["/etc/gitlab", "/var/opt/gitlab", "/var/log/gitlab"] # 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/README.md b/docker/README.md index 9507aa6a63c..e4d56cdb336 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,9 +1,6 @@ # GitLab Docker images -## What is GitLab? - -GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. -Learn more on [https://about.gitlab.com](https://about.gitlab.com) +The GitLab docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). ## After starting a container @@ -11,152 +8,162 @@ After starting a container you can go to [http://localhost:8080/](http://localho It might take a while before the docker container is responding to queries. -You can check the status with something like `sudo docker logs -f 7c10172d7705`. +You can check the status with something like `sudo docker logs -f gitlab`. -You can login to the web interface with username `root` and password `password`. +You can login to the web interface with username `root` and password `5iveL!fe`. Next time, you can just use docker start and stop to run the container. -## How to build the docker images +## Run the image -This guide will also let you know how to build docker images yourself. -Please run all the commands from the GitLab repo root directory. -People using boot2docker should run all the commands without sudo. +Run the image: +```bash +sudo docker run --detach \ + --publish 8443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest +``` -## Choosing between the single and the app and data images +This will download and start GitLab CE container and publish ports needed to access SSH, HTTP and HTTPS. +All GitLab data will be stored as subdirectories of `/srv/gitlab/`. +The container will automatically `restart` after system reboot. -Normally docker uses a single image for one applications. -But GitLab stores repositories and uploads in the filesystem. -This means that upgrades of a single image are hard. -That is why we recommend using separate app and data images. -We'll first describe how to use a single image. -After that we'll describe how to use the app and data images. +After this you can login to the web interface as explained above in 'After starting a container'. -## Single image +## Where is the data stored? -Get a published image from Dockerhub: +The GitLab container uses host mounted volumes to store persistent data: +- `/srv/gitlab/data` mounted as `/var/opt/gitlab` in the container is used for storing *application data* +- `/srv/gitlab/logs` mounted as `/var/log/gitlab` in the container is used for storing *logs* +- `/srv/gitlab/config` mounted as `/etc/gitlab` in the container is used for storing *configuration* -```bash -sudo docker pull sytse/gitlab-ce:7.10.1 -``` +You can fine tune these directories to meet your requirements. -Run the image: +### Configure GitLab + +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 bash in a new the context of running container, you will be able to browse all directories and use your favorite text editor: ```bash -sudo docker run --detach --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1 +sudo docker exec -it gitlab /bin/bash ``` -After this you can login to the web interface as explained above in 'After starting a container'. - -Build the image: - +You can also edit just `/etc/gitlab/gitlab.rb`: ```bash -sudo docker build --tag sytse/gitlab-ce:7.10.1 docker/single/ +sudo docker exec -it gitlab vi /etc/gitlab/gitlab.rb ``` -Publish the image to Dockerhub: +**You should set the `external_url` to point to a valid URL.** -```bash -sudo docker push sytse/gitlab-ce -``` +**You may also be interesting in [Enabling HTTPS](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#enable-https).** + +**To receive e-mails from GitLab you have to configure the [SMTP settings](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/smtp.md), +because Docker image doesn't have a SMTP server.** -Diagnosing commands: +**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab: ```bash -sudo docker run -i -t sytse/gitlab-ce:7.10.1 -sudo docker run -ti -e TERM=linux --name gitlab-ce-troubleshoot --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1 bash /usr/local/bin/wrapper +sudo docker restart gitlab ``` -## App and data images +For more options for configuring the container please check [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). -### Get published images from Dockerhub +## Diagnose potential problems +Read container logs: ```bash -sudo docker pull sytse/gitlab-data -sudo docker pull sytse/gitlab-app:7.10.1 +sudo docker logs gitlab ``` -### Run the images - +Enter running container: ```bash -sudo docker run --name gitlab-data sytse/gitlab-data /bin/true -sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data sytse/gitlab-app:7.10.1 +sudo docker exec -it gitlab /bin/bash ``` -After this you can login to the web interface as explained above in 'After starting a container'. - -### Build images +From within container you can administrer GitLab container as you would normally administer Omnibus installation: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md. -Build your own based on the Omnibus packages with the following commands. +### Upgrade GitLab to newer version +To upgrade GitLab to new version you have to do: +1. pull new image, ```bash -sudo docker build --tag gitlab-data docker/data/ -sudo docker build --tag gitlab-app:7.10.1 docker/app/ +sudo docker stop gitlab ``` -After this run the images: - +1. stop running container, ```bash -sudo docker run --name gitlab-data gitlab-data /bin/true -sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data gitlab-app:7.10.1 +sudo docker rm gitlab ``` -We assume using a data volume container, this will simplify migrations and backups. -This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it. - -The directories on data container are: - -- `/var/opt/gitlab` for application data -- `/var/log/gitlab` for logs -- `/etc/gitlab` for configuration - -### Configure GitLab - -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: +1. remove existing container, +```bash +sudo docker pull gitlab/gitlab-ce:latest +``` +1. create the container once again with previously specified options. ```bash -sudo docker run -ti -e TERM=linux --rm --volumes-from gitlab-data ubuntu -vi /etc/gitlab/gitlab.rb +sudo docker run --detach \ + --publish 8443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest ``` -**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab. +On the first run GitLab will reconfigure and update itself. -You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). +### Run GitLab CE on public IP address -### Upgrade GitLab with app and data images +You can make Docker to use your IP address and forward all traffic to the GitLab CE container. +You can do that by modifying the `--publish` ([Binding container ports to the host](https://docs.docker.com/articles/networking/#binding-ports)): -To upgrade GitLab to new versions, stop running container, create new docker image and container from that image. +> --publish=[] : Publish a container᾿s port or a range of ports to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort -It Assumes that you're upgrading from 7.8.1 to 7.10.1 and you're in the updated GitLab repo root directory: +To expose GitLab CE on IP 1.1.1.1: ```bash -sudo docker stop gitlab-app -sudo docker rm gitlab-app -sudo docker build --tag gitlab-app:7.10.1 docker/app/ -sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab-data gitlab-app:7.10.1 +sudo docker run --detach \ + --publish 1.1.1.1:443:443 --publish 1.1.1.1:80:80 --publish 1.1.1.1:22:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest ``` -On the first run GitLab will reconfigure and update itself. If everything runs OK don't forget to cleanup the app image: +You can then access GitLab instance at http://1.1.1.1/ and https://1.1.1.1/. + +### Build the image + +This guide will also let you know how to build docker image yourself. +Please run the command from the GitLab repo root directory. +People using boot2docker should run all the commands without sudo. ```bash -sudo docker rmi gitlab-app:7.8.1 +sudo docker build --tag gitlab/gitlab-ce:latest docker/ ``` -### Publish images to Dockerhub +### Publish the image to Dockerhub - Ensure the containers are running - Login to Dockerhub with `sudo docker login` -- Run the following (replace '7.10.1' with the version you're using and 'Sytse Sijbrandij' with your name): ```bash -sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-app sytse/gitlab-app:7.10.1 -sudo docker push sytse/gitlab-app:7.10.1 -sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-data sytse/gitlab-data -sudo docker push sytse/gitlab-data +sudo docker login +sudo docker push gitlab/gitlab-ce:latest ``` ## Troubleshooting Please see the [troubleshooting](troubleshooting.md) file in this directory. + +Note: We use `fig.yml` to have compatibility with fig and because docker-compose also supports it. + +Our docker image runs chef at every start to generate GitLab configuration. diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile deleted file mode 100644 index fe3f7f0bcd2..00000000000 --- a/docker/app/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM ubuntu:14.04 - -# Install required packages -RUN apt-get update -q \ - && DEBIAN_FRONTEND=noninteractive apt-get install -qy --no-install-recommends \ - ca-certificates \ - openssh-server \ - wget \ - apt-transport-https - -# Download & Install GitLab -# If you run GitLab Enterprise Edition point it to a location where you have downloaded it. -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 \ - && mkfifo /opt/gitlab/sv/sshd/supervise/ok \ - && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \ - && chmod a+x /opt/gitlab/sv/sshd/run \ - && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ - && mkdir -p /var/run/sshd - -# Expose web & ssh -EXPOSE 80 22 - -# Copy assets -COPY assets/wrapper /usr/local/bin/ - -# Wrapper to handle signal, trigger runit and reconfigure GitLab -CMD ["/usr/local/bin/wrapper"]
\ No newline at end of file diff --git a/docker/app/assets/wrapper b/docker/app/assets/wrapper deleted file mode 100755 index 9e6e7a05903..00000000000 --- a/docker/app/assets/wrapper +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -function sigterm_handler() { - echo "SIGTERM signal received, try to gracefully shutdown all services..." - gitlab-ctl stop -} - -trap "sigterm_handler; exit" TERM - -function entrypoint() { - # Default is to run runit and reconfigure GitLab - gitlab-ctl reconfigure & - /opt/gitlab/embedded/bin/runsvdir-start & - wait -} - -entrypoint diff --git a/docker/single/assets/wrapper b/docker/assets/wrapper index 966b2cab4a1..8bc8370fbc9 100755 --- a/docker/single/assets/wrapper +++ b/docker/assets/wrapper @@ -13,4 +13,9 @@ function entrypoint() { gitlab-ctl tail # tail all logs } +if [[ ! -e /etc/gitlab/gitlab.rb ]]; then + cp /assets/gitlab.rb /etc/gitlab/gitlab.rb + chmod 0600 /etc/gitlab/gitlab.rb +fi + entrypoint diff --git a/docker/data/Dockerfile b/docker/data/Dockerfile deleted file mode 100644 index ea0175c4aa2..00000000000 --- a/docker/data/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM busybox - -# Declare volumes -VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] -# Copy assets -COPY assets/gitlab.rb /etc/gitlab/ - -CMD /bin/sh diff --git a/docker/data/assets/gitlab.rb b/docker/data/assets/gitlab.rb deleted file mode 100644 index 7fddf309c01..00000000000 --- a/docker/data/assets/gitlab.rb +++ /dev/null @@ -1,37 +0,0 @@ -# External URL should be your Docker instance. -# By default, this example is the "standard" boot2docker IP. -# 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/docker/fig.yml b/docker/fig.yml new file mode 100644 index 00000000000..989551cbfe2 --- /dev/null +++ b/docker/fig.yml @@ -0,0 +1,2 @@ +app: + build: . diff --git a/docker/marathon.json b/docker/marathon.json new file mode 100644 index 00000000000..9b2091a8c22 --- /dev/null +++ b/docker/marathon.json @@ -0,0 +1,31 @@ +{ + "id": "/gitlab", + "ports": [0,0], + "cpus": 2, + "mem": 2048.0, + "disk": 10240.0, + "container": { + "type": "DOCKER", + "docker": { + "network": "HOST", + "image": "gitlab/gitlab-ce:latest" + }, + "volumes": [ + { + "containerPath": "/etc/gitlab", + "hostPath": "/var/data/etc/gitlab", + "mode": "RW" + }, + { + "containerPath": "/var/opt/gitlab", + "hostPath": "/var/data/opt/gitlab", + "mode": "RW" + }, + { + "containerPath": "/var/log/gitlab", + "hostPath": "/var/data/log/gitlab", + "mode": "RW" + } + ] + } +}
\ No newline at end of file diff --git a/docker/single/assets/gitlab.rb b/docker/single/assets/gitlab.rb deleted file mode 100644 index ef84e7832d6..00000000000 --- a/docker/single/assets/gitlab.rb +++ /dev/null @@ -1,37 +0,0 @@ -# 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/docker/single/marathon.json b/docker/single/marathon.json deleted file mode 100644 index d23c2b84e0e..00000000000 --- a/docker/single/marathon.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "/gitlab", - "ports": [0,0], - "cpus": 2, - "mem": 2048.0, - "disk": 10240.0, - "container": { - "type": "DOCKER", - "docker": { - "network": "HOST", - "image": "sytse/gitlab-ce:7.10.1" - } - } -}
\ No newline at end of file diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md index 5827f2185db..63482547daa 100644 --- a/docker/troubleshooting.md +++ b/docker/troubleshooting.md @@ -9,24 +9,19 @@ postgresql['log_directory'] = '/var/log/gitlab/postgresql' # Commands ```bash -sudo docker build --tag gitlab_image docker/ +sudo docker build --tag gitlab/gitlab-ce:latest docker/ -sudo docker rm -f gitlab_app -sudo docker rm -f gitlab_data +sudo docker rm -f gitlab -sudo docker run --name gitlab_data gitlab_image /bin/true +sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb -sudo docker run -ti --rm --volumes-from gitlab_data ubuntu apt-get update && sudo apt-get install -y vim && sudo vim /etc/gitlab/gitlab.rb +sudo docker exec gitlab tail -f /var/log/gitlab/reconfigure.log -sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image +sudo docker exec gitlab tail -f /var/log/gitlab/postgresql/current -sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/reconfigure.log +sudo docker exec gitlab cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers -sudo docker run -t --rm --volumes-from gitlab_data ubuntu tail -f /var/log/gitlab/postgresql/current - -sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers - -sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab.rb +sudo docker exec gitlab cat /etc/gitlab/gitlab.rb ``` # Interactively @@ -37,7 +32,16 @@ sudo docker run -t --rm --volumes-from gitlab_data ubuntu cat /etc/gitlab/gitlab # - we run interactively (-t -i) # - we define TERM=linux because it allows to use arrow keys in vi (!!!) # - we choose another startup command (bash) -sudo docker run -ti -e TERM=linux --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image bash +sudo docker run --ti \ + -e TERM=linux + --publish 80443:443 --publish 8080:80 --publish 2222:22 \ + --name gitlab \ + --restart always \ + --volume /srv/gitlab/config:/etc/gitlab \ + --volume /srv/gitlab/logs:/var/log/gitlab \ + --volume /srv/gitlab/data:/var/opt/gitlab \ + gitlab/gitlab-ce:latest \ + bash # Configure GitLab to redirect PostgreSQL logs echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb @@ -64,10 +68,17 @@ free -m # Cleanup -Remove ALL docker containers and images (also non GitLab ones): +Remove ALL docker containers and images (also non GitLab ones). +**Be careful, because the `-v` also removes volumes attached to the images.** -``` -docker rm $(docker ps -a -q) +```bash +# Remove all containers with attached volumes +docker rm -v $(docker ps -a -q) + +# Remove all images docker rmi $(docker images -q) + +# Remove GitLab persistent data +rm -rf /srv/gitlab ``` diff --git a/features/abuse_report.feature b/features/abuse_report.feature new file mode 100644 index 00000000000..3e1cb455b77 --- /dev/null +++ b/features/abuse_report.feature @@ -0,0 +1,10 @@ +Feature: Abuse reports + Background: + Given I sign in as a user + And user "Mike" exists + + Scenario: Report abuse + Given I visit "Mike" user page + And I click "Report abuse" button + When I fill and submit abuse form + Then I should see success message diff --git a/features/admin/abuse_report.feature b/features/admin/abuse_report.feature new file mode 100644 index 00000000000..7d4ec2556e5 --- /dev/null +++ b/features/admin/abuse_report.feature @@ -0,0 +1,8 @@ +Feature: Admin Abuse reports + Background: + Given I sign in as an admin + And abuse reports exist + + Scenario: Browse abuse reports + When I visit abuse reports page + Then I should see list of abuse reports diff --git a/features/groups.feature b/features/groups.feature index 415e43d6ae7..299e846edb0 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -4,6 +4,10 @@ Feature: Groups And "John Doe" is owner of group "Owned" And "John Doe" is guest of group "Guest" + Scenario: I should have back to group button + When I visit group "Owned" page + Then I should see back to dashboard button + @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/profile/profile.feature b/features/profile/profile.feature index 7a1345f2b37..27c0bde364e 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -35,6 +35,7 @@ Feature: Profile Then I change my avatar And I should see new avatar And I should see the "Remove avatar" button + And I should see the gravatar host link Scenario: I remove my avatar Given I visit profile page @@ -42,6 +43,7 @@ Feature: Profile When I remove my avatar Then I should see my gravatar And I should not see the "Remove avatar" button + And I should see the gravatar host link Scenario: My password is expired Given my password is expired diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index 9ac65b1257c..bfbaaec5a35 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -17,6 +17,10 @@ Feature: Project Issues Milestones And I submit new milestone "v2.3" Then I should see milestone "v2.3" + Scenario: I delete new milestone + Given I click link to remove milestone "v2.2" + And I should see no milestones + @javascript Scenario: Listing closed issues Given the milestone has open and closed issues diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature index 8beb6043aff..6cc89a15a78 100644 --- a/features/project/network_graph.feature +++ b/features/project/network_graph.feature @@ -11,6 +11,11 @@ Feature: Project Network Graph And page should have "master" on graph @javascript + Scenario: I should see project network with 'test' branch + When I visit project network page on branch 'test' + Then page should have 'test' on graph + + @javascript Scenario: I should switch "branch" and "tag" When I switch ref to "feature" Then page should select "feature" in select box diff --git a/features/project/project.feature b/features/project/project.feature index 5fb2c67401e..089ffcba14a 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,6 +18,15 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button + Scenario: I should have back to group button + And project "Shop" belongs to group + And I visit project "Shop" page + Then I should see back to group button + + Scenario: I should have back to group button + And I visit project "Shop" page + Then I should see back to dashboard button + Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index af68cb96ed9..d3a77466a35 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -158,3 +158,10 @@ Feature: Project Source Browse Files Given I visit project source page for "6d394385cf567f80a8fd85055db1ab4c5295806f" And I click on ".gitignore" file in repo Then I don't see the permalink link + + @javascript + Scenario: I browse code with single quotes in the ref + Given I switch ref to 'test' + And I see the ref 'test' has been selected + And I visit the 'test' tree + Then I see the commit data diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb new file mode 100644 index 00000000000..8f9ddb2899f --- /dev/null +++ b/features/steps/abuse_reports.rb @@ -0,0 +1,28 @@ +class Spinach::Features::AbuseReports < Spinach::FeatureSteps + include SharedAuthentication + + step 'I visit "Mike" user page' do + visit user_path(user_mike) + end + + step 'I click "Report abuse" button' do + click_link 'Report abuse' + end + + step 'I fill and submit abuse form' do + fill_in 'abuse_report_message', with: 'This user send spam' + click_button 'Send report' + end + + step 'I should see success message' do + page.should have_content 'Thank you for your report' + end + + step 'user "Mike" exists' do + user_mike + end + + def user_mike + @user_mike ||= create(:user, name: 'Mike') + end +end diff --git a/features/steps/admin/abuse_reports.rb b/features/steps/admin/abuse_reports.rb new file mode 100644 index 00000000000..0149416c919 --- /dev/null +++ b/features/steps/admin/abuse_reports.rb @@ -0,0 +1,15 @@ +class Spinach::Features::AdminAbuseReports < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'I should see list of abuse reports' do + page.should have_content("Abuse Reports") + page.should have_content AbuseReport.first.message + page.should have_link("Remove user") + end + + step 'abuse reports exist' do + create(:abuse_report) + end +end diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 9cc74a97c3a..83a3f48abe3 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -58,7 +58,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end step 'we have user "John Doe" in group' do - current_group.add_user(user_john, Gitlab::Access::REPORTER) + current_group.add_reporter(user_john) end step 'I remove user "John Doe" from group' do diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 147a4bd7486..7a6aec23af8 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -6,7 +6,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps step 'I modify settings and save form' do uncheck 'Gravatar enabled' - fill_in 'Home page url', with: 'https://about.gitlab.com/' + fill_in 'Home page URL', with: 'https://about.gitlab.com/' click_button 'Save' end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index 6c4b91586d6..2e17d5c4c2e 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -3,6 +3,14 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps include SharedPaths include SharedAdmin + before do + allow(Devise).to receive(:omniauth_providers).and_return([:twitter, :twitter_updated]) + end + + after do + allow(Devise).to receive(:omniauth_providers).and_call_original + end + step 'I should see all users' do User.all.each do |user| expect(page).to have_content user.name @@ -71,7 +79,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps project.team << [user, :developer] group = create(:group) - group.add_user(user, Gitlab::Access::DEVELOPER) + group.add_developer(user) end step 'click on "Mike" link' do @@ -121,7 +129,6 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I visit "Pete" identities page in admin' do - allow(Gitlab::OAuth::Provider).to receive(:names).and_return(%w(twitter twitter_updated)) visit admin_user_identities_path(@user) end diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 89b82293ef2..87cd33c37eb 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -19,7 +19,7 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps step '"John Doe" is owner of group "TestGroup"' do group = Group.find_by(name: "TestGroup") || create(:group, name: "TestGroup") user = create(:user, name: "John Doe") - group.add_user(user, Gitlab::Access::OWNER) + group.add_owner(user) end step 'I visit group "TestGroup" page' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 2812c5473e9..46e1f4d0990 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,6 +5,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'I should see back to dashboard button' do + expect(page).to have_content 'Back to Dashboard' + end + step 'gitlab user "Mike"' do create(:user, name: "Mike") end diff --git a/features/steps/invites.rb b/features/steps/invites.rb index d051cc3edc8..5e8feff5095 100644 --- a/features/steps/invites.rb +++ b/features/steps/invites.rb @@ -6,7 +6,7 @@ class Spinach::Features::Invites < Spinach::FeatureSteps step '"John Doe" has invited "user@example.com" to group "Owned"' do user = User.find_by(name: "John Doe") group = Group.find_by(name: "Owned") - group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user) + group.add_developer("user@example.com", user) end step 'I visit the invitation page' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 2b6b8b167f6..8cf24705a5e 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -59,6 +59,10 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I should not see the "Remove avatar" button' do expect(page).not_to have_link("Remove avatar") end + + step 'I should see the gravatar host link' do + expect(page).to have_link("gravatar.com") + end step 'I try change my password w/o old one' do page.within '.update-password' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 58c16d59d05..3e97e84d116 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -138,10 +138,11 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I should see the users from the target project ID' do - expect(page).to have_selector('.user-result', visible: true, count: 2) + expect(page).to have_selector('.user-result', visible: true, count: 3) users = page.all('.user-name') expect(users[0].text).to eq 'Unassigned' - expect(users[1].text).to eq @project.users.first.name + expect(users[1].text).to eq current_user.name + expect(users[2].text).to eq @project.users.first.name end # Verify a link is generated against the correct project diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 708c5243947..61e62c2adbd 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -56,4 +56,12 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps step 'I should see 3 issues' do expect(page).to have_selector('#tab-issues li.issue-row', count: 4) end + + step 'I click link to remove milestone "v2.2"' do + click_link 'Remove' + end + + step 'I should see no milestones' do + expect(page).to have_content('No milestones to show') + end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index a1a26abd8ca..04784207a1a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see new target branch changes' do - expect(page).to have_content 'From fix into feature' + expect(page).to have_content 'Request to merge fix into feature' expect(page).to have_content 'Target branch changed from master to feature' end diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 992cf2734fd..7a83d32a240 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -11,8 +11,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) - project = Project.find_by(name: "Shop") - visit namespace_project_network_path(project.namespace, project, "master") + @project = Project.find_by(name: "Shop") + visit namespace_project_network_path(@project.namespace, @project, "master") + end + + step "I visit project network page on branch 'test'" do + visit namespace_project_network_path(@project.namespace, @project, "'test'") end step 'page should select "master" in select box' do @@ -29,6 +33,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end end + step "page should have 'test' on graph" do + page.within '.network-graph' do + expect(page).to have_content "'test'" + end + end + When 'I switch ref to "feature"' do select 'feature', from: 'ref' sleep 2 diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index b4a0ba1e27f..0404fd5e594 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -86,13 +86,15 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Forum" README' do - expect(page).to have_link 'README.md' - expect(page).to have_content 'Sample repo for testing gitlab features' + page.within('#README') do + expect(page).to have_content 'Sample repo for testing gitlab features' + end end step 'I should see project "Shop" README' do - expect(page).to have_link 'README.md' - expect(page).to have_content 'testme' + page.within('#README') do + expect(page).to have_content 'testme' + end end step 'I add project tags' do @@ -114,4 +116,18 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should not see "Snippets" button' do expect(page).not_to have_link 'Snippets' end + + step 'project "Shop" belongs to group' do + group = create(:group) + @project.namespace = group + @project.save! + end + + step 'I should see back to dashboard button' do + expect(page).to have_content 'Back to Dashboard' + end + + step 'I should see back to group button' do + expect(page).to have_content 'Back to Group' + end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 95879b9544d..5cb085db207 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -193,6 +193,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) end + step "I switch ref to 'test'" do + select "'test'", from: 'ref' + end + + step "I see the ref 'test' has been selected" do + expect(page).to have_selector '.select2-chosen', text: "'test'" + end + + step "I visit the 'test' tree" do + visit namespace_project_tree_path(@project.namespace, @project, "'test'") + end + + step 'I see the commit data' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + private def set_new_content diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 88a98a37807..bb0cd9ac105 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -139,6 +139,10 @@ module SharedPaths visit admin_root_path end + step 'I visit abuse reports page' do + visit admin_abuse_reports_path + end + step 'I visit admin projects page' do visit admin_namespaces_projects_path end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c6108bee68b..1f9dd6bc152 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -6,6 +6,10 @@ module API class UserBasic < UserSafe expose :id, :state, :avatar_url + + expose :web_url do |user, options| + Rails.application.routes.url_helpers.user_url(user) + end end class User < UserBasic @@ -31,6 +35,10 @@ module API expose :private_token end + class Email < Grape::Entity + expose :id, :email + end + class Hook < Grape::Entity expose :id, :url, :created_at end @@ -59,6 +67,7 @@ module API expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url + expose :star_count, :forks_count end class ProjectMember < UserBasic @@ -69,6 +78,11 @@ module API class Group < Grape::Entity expose :id, :name, :path, :description + expose :avatar_url + + expose :web_url do |group, options| + Rails.application.routes.url_helpers.group_url(group) + end end class GroupDetail < Group @@ -171,6 +185,7 @@ module API expose :source_project_id, :target_project_id expose :label_names, as: :labels expose :description + expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone end @@ -190,6 +205,9 @@ module API expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic expose :created_at + expose :system?, as: :system + expose :upvote?, as: :upvote + expose :downvote?, as: :downvote end class MRNote < Grape::Entity diff --git a/lib/api/files.rb b/lib/api/files.rb index c7b30cf2f07..83581cd3990 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -3,26 +3,6 @@ module API class Files < Grape::API before { authenticate! } - helpers do - def commit_params(attrs) - { - file_path: attrs[:file_path], - current_branch: attrs[:branch_name], - target_branch: attrs[:branch_name], - commit_message: attrs[:commit_message], - file_content: attrs[:content], - file_content_encoding: attrs[:encoding] - } - end - - def commit_response(attrs) - { - file_path: attrs[:file_path], - branch_name: attrs[:branch_name], - } - end - end - resource :projects do # Get file from repository # File content is Base64 encoded @@ -67,7 +47,7 @@ module API file_path: blob.path, size: blob.size, encoding: "base64", - content: Base64.encode64(blob.data), + content: Base64.strict_encode64(blob.data), ref: ref, blob_id: blob.id, commit_id: commit.id, @@ -93,11 +73,17 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute + branch_name = attrs.delete(:branch_name) + file_path = attrs.delete(:file_path) + result = ::Files::CreateService.new(user_project, current_user, attrs, branch_name, file_path).execute if result[:status] == :success status(201) - commit_response(attrs) + + { + file_path: file_path, + branch_name: branch_name + } else render_api_error!(result[:message], 400) end @@ -119,11 +105,17 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute + branch_name = attrs.delete(:branch_name) + file_path = attrs.delete(:file_path) + result = ::Files::UpdateService.new(user_project, current_user, attrs, branch_name, file_path).execute if result[:status] == :success status(200) - commit_response(attrs) + + { + file_path: file_path, + branch_name: branch_name + } else http_status = result[:http_status] || 400 render_api_error!(result[:message], http_status) @@ -146,11 +138,17 @@ module API required_attributes! [:file_path, :branch_name, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :commit_message] - result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute + branch_name = attrs.delete(:branch_name) + file_path = attrs.delete(:file_path) + result = ::Files::DeleteService.new(user_project, current_user, attrs, branch_name, file_path).execute if result[:status] == :success status(200) - commit_response(attrs) + + { + file_path: file_path, + branch_name: branch_name + } else render_api_error!(result[:message], 400) end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index e88b6e31775..024aeec2e14 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -74,9 +74,9 @@ module API # POST /groups/:id/projects/:project_id post ":id/projects/:project_id" do authenticated_as_admin! - group = Group.find(params[:id]) + group = Group.find_by(id: params[:id]) project = Project.find(params[:project_id]) - result = ::Projects::TransferService.new(project, current_user, namespace_id: group.id).execute + result = ::Projects::TransferService.new(project, current_user).execute(group) if result present group diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index aa43e1dffd9..ce21c699e8f 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -229,7 +229,7 @@ module API authorize! :read_merge_request, merge_request - present paginate(merge_request.notes), with: Entities::MRNote + present paginate(merge_request.notes.fresh), with: Entities::MRNote end # Post comment to merge request diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 3726be7c537..3efdfe2d46e 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -78,17 +78,15 @@ module API put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do required_attributes! [:body] - authorize! :admin_note, user_project.notes.find(params[:note_id]) + note = user_project.notes.find(params[:note_id]) + + authorize! :admin_note, note opts = { - note: params[:body], - note_id: params[:note_id], - noteable_type: noteables_str.classify, - noteable_id: params[noteable_id_str] + note: params[:body] } - @note = ::Notes::UpdateService.new(user_project, current_user, - opts).execute + @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) if @note.valid? present @note, with: Entities::Note diff --git a/lib/api/users.rb b/lib/api/users.rb index c468371d3d4..ee29f952246 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -131,11 +131,11 @@ module API # Add ssh key to a specified user. Only available to admin users. # # Parameters: - # id (required) - The ID of a user - # key (required) - New SSH Key - # title (required) - New SSH Key's title + # id (required) - The ID of a user + # key (required) - New SSH Key + # title (required) - New SSH Key's title # Example Request: - # POST /users/:id/keys + # POST /users/:id/keys post ":id/keys" do authenticated_as_admin! required_attributes! [:title, :key] @@ -153,9 +153,9 @@ module API # Get ssh keys of a specified user. Only available to admin users. # # Parameters: - # uid (required) - The ID of a user + # uid (required) - The ID of a user # Example Request: - # GET /users/:uid/keys + # GET /users/:uid/keys get ':uid/keys' do authenticated_as_admin! user = User.find_by(id: params[:uid]) @@ -185,6 +185,65 @@ module API end end + # Add email to a specified user. Only available to admin users. + # + # Parameters: + # id (required) - The ID of a user + # email (required) - Email address + # Example Request: + # POST /users/:id/emails + post ":id/emails" do + authenticated_as_admin! + required_attributes! [:email] + + user = User.find(params[:id]) + attrs = attributes_for_keys [:email] + email = user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Get emails of a specified user. Only available to admin users. + # + # Parameters: + # uid (required) - The ID of a user + # Example Request: + # GET /users/:uid/emails + get ':uid/emails' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + present user.emails, with: Entities::Email + end + + # Delete existing email of a specified user. Only available to admin + # users. + # + # Parameters: + # uid (required) - The ID of a user + # id (required) - Email ID + # Example Request: + # DELETE /users/:uid/emails/:id + delete ':uid/emails/:id' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + begin + email = user.emails.find params[:id] + email.destroy + + user.update_secondary_emails! + rescue ActiveRecord::RecordNotFound + not_found!('Email') + end + end + # Delete user. Available only for admin # # Example Request: @@ -289,6 +348,58 @@ module API rescue end end + + # Get currently authenticated user's emails + # + # Example Request: + # GET /user/emails + get "emails" do + present current_user.emails, with: Entities::Email + end + + # Get single email owned by currently authenticated user + # + # Example Request: + # GET /user/emails/:id + get "emails/:id" do + email = current_user.emails.find params[:id] + present email, with: Entities::Email + end + + # Add new email to currently authenticated user + # + # Parameters: + # email (required) - Email address + # Example Request: + # POST /user/emails + post "emails" do + required_attributes! [:email] + + attrs = attributes_for_keys [:email] + email = current_user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Delete existing email of currently authenticated user + # + # Parameters: + # id (required) - EMail ID + # Example Request: + # DELETE /user/emails/:id + delete "emails/:id" do + begin + email = current_user.emails.find params[:id] + email.destroy + + current_user.update_secondary_emails! + rescue + end + end end end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index b8aa6b9ff2f..939f28fc1c6 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -7,13 +7,19 @@ module Backup def initialize @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] @db_dir = File.join(Gitlab.config.backup.path, 'db') - FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) + FileUtils.rm_rf(@db_dir) + # Ensure the parent dir of @db_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create @db_dir before us + FileUtils.mkdir(@db_dir, mode: 0700) end def dump success = case config["adapter"] when /^mysql/ then $progress.print "Dumping MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysqldump', *mysql_args, config['database'], out: db_file_name) when "postgresql" then $progress.print "Dumping PostgreSQL database #{config['database']} ... " @@ -39,6 +45,8 @@ module Backup success = case config["adapter"] when /^mysql/ then $progress.print "Restoring MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysql', *mysql_args, config['database'], in: db_file_name) when "postgresql" then $progress.print "Restoring PostgreSQL database #{config['database']} ... " @@ -65,8 +73,7 @@ module Backup 'port' => '--port', 'socket' => '--socket', 'username' => '--user', - 'encoding' => '--default-character-set', - 'password' => '--password' + 'encoding' => '--default-character-set' } args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 6fa2079d1a8..13c68d9354f 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -16,18 +16,16 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod(0700, folders_to_backup) - # create archive $progress.print "Creating backup archive: #{tar_file} ... " - orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *backup_contents) + # Set file permissions on open to prevent chmod races. + tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} + if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end - File.umask(orig_umask) upload(tar_file) end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index dfb2da9f84e..4d70f7883dd 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -130,7 +130,10 @@ module Backup def prepare FileUtils.rm_rf(backup_repos_path) - FileUtils.mkdir_p(backup_repos_path) + # Ensure the parent dir of backup_repos_path exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_repos_path before us + FileUtils.mkdir(backup_repos_path, mode: 0700) end def silent diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index bf43610acf6..1f9626644e6 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -10,7 +10,11 @@ module Backup # Copy uploads from public/uploads to backup/uploads def dump - FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.rm_rf(backup_uploads_dir) + # Ensure the parent dir of backup_uploads_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_uploads_dir before us + FileUtils.mkdir(backup_uploads_dir, mode: 0700) FileUtils.cp_r(app_uploads_dir, backup_dir) end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 3f420553d42..322aed5e27c 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -94,7 +94,7 @@ module ExtractsPath @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) - @id = get_id + @id = Addressable::URI.unescape(get_id) @ref, @path = extract_ref(@id) @repo = @project.repository if @options[:extended_sha1].blank? diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 03cef30c97d..12292f614e9 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -26,7 +26,12 @@ module Grack auth! if project && authorized_request? - @app.call(env) + if ENV['GITLAB_GRACK_AUTH_ONLY'] == '1' + # Tell gitlab-git-http-server the request is OK, and what the GL_ID is + render_grack_auth_ok + else + @app.call(env) + end elsif @user.nil? && !@gitlab_ci unauthorized else @@ -174,6 +179,10 @@ module Grack end end + def render_grack_auth_ok + [200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user) })]] + end + def render_not_found [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb index 17ec029eed4..9f5adee594a 100644 --- a/lib/gitlab/backend/shell_env.rb +++ b/lib/gitlab/backend/shell_env.rb @@ -7,7 +7,7 @@ module Gitlab def set_env(user) # Set GL_ID env variable if user - ENV['GL_ID'] = "user-#{user.id}" + ENV['GL_ID'] = gl_id(user) end end @@ -15,5 +15,14 @@ module Gitlab # Reset GL_ID env variable ENV['GL_ID'] = nil end + + def gl_id(user) + if user.present? + "user-#{user.id}" + else + # This empty string is used in the render_grack_auth_ok method + "" + end + end end end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 5b1952b9675..aec44b8c87b 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -1,6 +1,8 @@ module Gitlab module BitbucketImport class Client + class Unauthorized < StandardError; end + attr_reader :consumer, :api def initialize(access_token = nil, access_token_secret = nil) @@ -46,23 +48,23 @@ module Gitlab end def user - JSON.parse(api.get("/api/1.0/user").body) + JSON.parse(get("/api/1.0/user").body) end def issues(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues").body) end def issue_comments(project_identifier, issue_id) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) end def project(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body) end def find_deploy_key(project_identifier, key) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| deploy_key["key"].chomp == key.chomp end end @@ -82,11 +84,22 @@ module Gitlab end def projects - JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + end + + def incompatible_projects + JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" } end private + def get(url) + response = api.get(url) + raise Unauthorized if (400..499).include?(response.code.to_i) + + response + end + def config Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 70bfe059776..03c410726a5 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -327,7 +327,7 @@ module Gitlab link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" text = "[#{filename}](#{link})" - text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/ + text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i text end.compact end diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 3517ecdf5cf..99e7b529ba9 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -46,8 +46,11 @@ module Gitlab end last_the_same_symbols += 1 last_token = first_line[last_the_same_symbols..-1] - diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) - diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) + # This is tricky: escape backslashes so that `sub` doesn't interpret them + # as backreferences. Regexp.escape does NOT do the right thing. + replace_token = FINISH + last_token.gsub(/\\/, '\&\&') + diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) + diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) end diff_arr end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 889decc9b48..9f6e19a09fd 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -25,21 +25,11 @@ module Gitlab # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # options - options - # html_options - extra options for the reference links as given to link_to - def gfm(text, options = {}, html_options = {}) - gfm_with_options(text, options, html_options) - end - - # Public: Parse the provided text with GitLab-Flavored Markdown - # - # text - the source text # options - A Hash of options used to customize output (default: {}): # :xhtml - output XHTML instead of HTML # :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 = {}, html_options = {}) + def gfm(text, options = {}, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index 61591a9914b..a9f1ee9c161 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -57,10 +57,11 @@ module Gitlab title = range.reference_title klass = reference_class(:commit_range) + data = data_attribute(project.id) project_ref += '@' if project_ref - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{project_ref}#{range}</a>) else diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index f6932e76e70..eacdf8a6d37 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -47,10 +47,11 @@ module Gitlab title = escape_once(commit.link_title) klass = reference_class(:commit) + data = data_attribute(project.id) project_ref += '@' if project_ref - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{project_ref}#{commit.short_id}</a>) else diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index dea04761ead..ab6f6bc1cf7 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -49,8 +49,9 @@ module Gitlab title = escape_once("Issue: #{issue.title}") klass = reference_class(:issue) + data = data_attribute(project.id) - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{match}</a>) else diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index e022ca69c91..2186f36f854 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -43,8 +43,9 @@ module Gitlab url = url_for_label(project, label) klass = reference_class(:label) + data = data_attribute(project.id) - %(<a href="#{url}" + %(<a href="#{url}" #{data} class="#{klass}">#{render_colored_label(label)}</a>) else match diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 80779819485..884f60f9d53 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -47,10 +47,11 @@ module Gitlab title = escape_once("Merge Request: #{merge_request.title}") klass = reference_class(:merge_request) + data = data_attribute(project.id) url = url_for_merge_request(merge_request, project) - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{match}</a>) else diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a84bacd3d4f..47ee1d99da3 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -21,6 +21,22 @@ module Gitlab result[:references] = Hash.new { |hash, type| hash[type] = [] } end + # Returns a data attribute String to attach to a reference link + # + # id - Object ID + # type - Object type (default: :project) + # + # Examples: + # + # data_attribute(1) # => "data-project-id=\"1\"" + # data_attribute(2, :user) # => "data-user-id=\"2\"" + # data_attribute(3, :group) # => "data-group-id=\"3\"" + # + # Returns a String + def data_attribute(id, type = :project) + %Q(data-#{type}-id="#{id}") + end + def escape_once(html) ERB::Util.html_escape_once(html) end diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 9de2b24a9da..30f50b82996 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -98,15 +98,25 @@ module Gitlab # # Returns a String def path_type(path) - if repository.tree(current_sha, path).entries.any? + unescaped_path = Addressable::URI.unescape(path) + + if tree?(unescaped_path) 'tree' - elsif repository.blob_at(current_sha, path).try(:image?) + elsif image?(unescaped_path) 'raw' else 'blob' end end + def tree?(path) + repository.tree(current_sha, path).entries.any? + end + + def image?(path) + repository.blob_at(current_sha, path).try(:image?) + end + def current_sha context[:commit].try(:id) || ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index 174ba58af6c..92979a356dc 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -47,10 +47,11 @@ module Gitlab title = escape_once("Snippet: #{snippet.title}") klass = reference_class(:snippet) + data = data_attribute(project.id) url = url_for_snippet(snippet, project) - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{match}</a>) else diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index c9972957182..a4aec7a05d1 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -83,18 +83,20 @@ module Gitlab push_result(:user, *namespace.users) url = urls.group_url(group, only_path: context[:only_path]) + data = data_attribute(namespace.id, :group) text = Group.reference_prefix + group - %(<a href="#{url}" class="#{link_class}">#{text}</a>) + %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end def link_to_user(user, namespace) push_result(:user, namespace.owner) url = urls.user_url(user, only_path: context[:only_path]) + data = data_attribute(namespace.owner_id, :user) text = User.reference_prefix + user - %(<a href="#{url}" class="#{link_class}">#{text}</a>) + %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end def user_can_reference_group?(group) diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index f99be969d3e..a5f767b134d 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -21,7 +21,7 @@ module Gitlab # # Returns boolean def gitlab_markdown?(filename) - filename.downcase.end_with?(*%w(.mdown .md .markdown)) + filename.downcase.end_with?(*%w(.mdown .mkd .mkdn .md .markdown)) end # Public: Determines if the given filename has AsciiDoc extension. @@ -33,6 +33,16 @@ module Gitlab filename.downcase.end_with?(*%w(.adoc .ad .asciidoc)) end + # Public: Determines if the given filename is plain text. + # + # filename - Filename string to check + # + # Returns boolean + def plain?(filename) + filename.downcase.end_with?('.txt') || + filename.downcase == 'readme' + end + def previewable?(filename) markup?(filename) end diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 0f16c925900..9b8e783d16c 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -9,49 +9,63 @@ module Gitlab end def uid - Gitlab::Utils.force_utf8(auth_hash.uid.to_s) + @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s) end def provider - Gitlab::Utils.force_utf8(auth_hash.provider.to_s) + @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s) end def info auth_hash.info end - def name - Gitlab::Utils.force_utf8((info.try(:name) || full_name).to_s) + def get_info(key) + value = info.try(key) + Gitlab::Utils.force_utf8(value) if value + value end - def full_name - Gitlab::Utils.force_utf8("#{info.first_name} #{info.last_name}") + def name + @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}" end def username - Gitlab::Utils.force_utf8( - (info.try(:nickname) || generate_username).to_s - ) + @username ||= username_and_email[:username].to_s end def email - Gitlab::Utils.force_utf8( - (info.try(:email) || generate_temporarily_email).downcase - ) + @email ||= username_and_email[:email].to_s end def password - devise_friendly_token = Devise.friendly_token[0, 8].downcase - @password ||= Gitlab::Utils.force_utf8(devise_friendly_token) + @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase) + end + + private + + def username_and_email + @username_and_email ||= begin + username = get_info(:nickname) || get_info(:username) + email = get_info(:email) + + username ||= generate_username(email) if email + email ||= generate_temporarily_email(username) if username + + { + username: username, + email: email + } + end end # Get the first part of the email address (before @) # In addtion in removes illegal characters - def generate_username + def generate_username(email) email.match(/^[^@]*/)[0].parameterize end - def generate_temporarily_email + def generate_temporarily_email(username) "temp-email-for-oauth-#{username}@gitlab.localhost" end end diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb index f986499a27c..90c3fe8da33 100644 --- a/lib/gitlab/o_auth/provider.rb +++ b/lib/gitlab/o_auth/provider.rb @@ -1,18 +1,30 @@ module Gitlab module OAuth class Provider - def self.names - providers = [] + def self.providers + Devise.omniauth_providers + end - Gitlab.config.ldap.servers.values.each do |server| - providers << server['provider_name'] - end + def self.enabled?(name) + providers.include?(name.to_sym) + end - Gitlab.config.omniauth.providers.each do |provider| - providers << provider['name'] + def self.ldap_provider?(name) + name.to_s.start_with?('ldap') + end + + def self.config_for(name) + name = name.to_s + if ldap_provider?(name) + Gitlab::LDAP::Config.new(name).options + else + Gitlab.config.omniauth.providers.find { |provider| provider.name == name } end + end - providers + def self.label_for(name) + config = config_for(name) + (config && config['label']) || name.to_s.titleize end end end diff --git a/lib/gitlab/satellite/files/delete_file_action.rb b/lib/gitlab/satellite/files/delete_file_action.rb new file mode 100644 index 00000000000..0d37b9dea85 --- /dev/null +++ b/lib/gitlab/satellite/files/delete_file_action.rb @@ -0,0 +1,50 @@ +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 new file mode 100644 index 00000000000..3cb9c0b5ecb --- /dev/null +++ b/lib/gitlab/satellite/files/edit_file_action.rb @@ -0,0 +1,68 @@ +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 new file mode 100644 index 00000000000..6446b14568a --- /dev/null +++ b/lib/gitlab/satellite/files/file_action.rb @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000000..724dfa0d042 --- /dev/null +++ b/lib/gitlab/satellite/files/new_file_action.rb @@ -0,0 +1,67 @@ +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/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index 1f2e5f82dd5..52e8130956c 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -33,9 +33,10 @@ module Gitlab merge_repo.git.push(default_options, :origin, merge_request.target_branch) # remove source branch - if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) && !merge_request.for_fork? + if merge_request.remove_source_branch? # will raise CommandFailed when push fails merge_repo.git.push(default_options, :origin, ":#{merge_request.source_branch}") + merge_request.source_project.repository.expire_branch_names end # merge, push and branch removal successful true diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 582fc759efd..335dc44be19 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -47,6 +47,10 @@ module Gitlab def valid_level?(level) options.has_value?(level) end + + def allowed_fork_levels(origin_level) + [PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level } + end end def private? diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 2f7aff03c2a..f57b56cbdf0 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -22,10 +22,10 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML ERB::Util.html_escape_once(text) end - # Stolen from Rugments::Plugins::Redcarpet as this module is not required - # from Rugments's gem root. + # Stolen from Rouge::Plugins::Redcarpet as this module is not required + # from Rouge's gem root. def block_code(code, language) - lexer = Rugments::Lexer.find_fancy(language, code) || Rugments::Lexers::PlainText + lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText # XXX HACK: Redcarpet strips hard tabs out of code blocks, # so we assume you're not using leading spaces that aren't tabs, @@ -34,13 +34,13 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML code.gsub!(/^ /, "\t") end - formatter = Rugments::Formatters::HTML.new( + formatter = Rouge::Formatters::HTMLGitlab.new( cssclass: "code highlight #{@color_scheme} #{lexer.tag}" ) formatter.format(lexer.lex(code)) end def postprocess(full_document) - h.gfm_with_options(full_document, @options) + h.gfm(full_document, @options) end end diff --git a/lib/repository_cache.rb b/lib/repository_cache.rb index fa016a170cd..8ddc3511293 100644 --- a/lib/repository_cache.rb +++ b/lib/repository_cache.rb @@ -18,4 +18,12 @@ class RepositoryCache def fetch(key, &block) backend.fetch(cache_key(key), &block) end + + def exist?(key) + backend.exist?(cache_key(key)) + end + + def read(key) + backend.read(cache_key(key)) + end end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb new file mode 100644 index 00000000000..6762ca47c32 --- /dev/null +++ b/lib/rouge/formatters/html_gitlab.rb @@ -0,0 +1,177 @@ +require 'cgi' + +module Rouge + module Formatters + class HTMLGitlab < Rouge::Formatter + tag 'html_gitlab' + + # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance. + # + # [+nowrap+] If set to True, don't wrap the output at all, not + # even inside a <tt><pre></tt> tag (default: false). + # [+cssclass+] CSS class for the wrapping <tt><div></tt> tag + # (default: 'highlight'). + # [+linenos+] If set to 'table', output line numbers as a table + # with two cells, one containing the line numbers, + # the other the whole code. This is copy paste friendly, + # but may cause alignment problems with some browsers + # or fonts. If set to 'inline', the line numbers will + # be integrated in the <tt><pre></tt> tag that contains + # the code (default: nil). + # [+linenostart+] The line number for the first line (default: 1). + # [+lineanchors+] If set to true the formatter will wrap each output + # line in an anchor tag with a name of L-linenumber. + # This allows easy linking to certain lines + # (default: false). + # [+lineanchorsid+] If lineanchors is true the name of the anchors can + # be changed with lineanchorsid to e.g. foo-linenumber + # (default: 'L'). + # [+anchorlinenos+] If set to true, will wrap line numbers in <tt><a></tt> + # tags. Used in combination with linenos and lineanchors + # (default: false). + # [+inline_theme+] Inline CSS styles for the <pre> tag (default: false). + def initialize( + nowrap: false, + cssclass: 'highlight', + linenos: nil, + linenostart: 1, + lineanchors: false, + lineanchorsid: 'L', + anchorlinenos: false, + inline_theme: nil + ) + @nowrap = nowrap + @cssclass = cssclass + @linenos = linenos + @linenostart = linenostart + @lineanchors = lineanchors + @lineanchorsid = lineanchorsid + @anchorlinenos = anchorlinenos + @inline_theme = Theme.find(inline_theme).new if inline_theme.is_a?(String) + end + + def render(tokens) + case @linenos + when 'table' + render_tableized(tokens) + when 'inline' + render_untableized(tokens) + else + render_untableized(tokens) + end + end + + alias_method :format, :render + + private + + def render_untableized(tokens) + data = process_tokens(tokens) + + html = '' + html << "<pre class=\"#{@cssclass}\"><code>" unless @nowrap + html << wrap_lines(data[:code]) + html << "</code></pre>\n" unless @nowrap + html + end + + def render_tableized(tokens) + data = process_tokens(tokens) + + html = '' + html << "<div class=\"#{@cssclass}\">" unless @nowrap + html << '<table><tbody>' + html << "<td class=\"linenos\"><pre>" + html << wrap_linenos(data[:numbers]) + html << '</pre></td>' + html << "<td class=\"lines\"><pre><code>" + html << wrap_lines(data[:code]) + html << '</code></pre></td>' + html << '</tbody></table>' + html << '</div>' unless @nowrap + html + end + + def process_tokens(tokens) + rendered = [] + current_line = '' + + tokens.each do |tok, val| + # In the case of multi-line values (e.g. comments), we need to apply + # styling to each line since span elements are inline. + val.lines.each do |line| + stripped = line.chomp + current_line << span(tok, stripped) + + if line.end_with?("\n") + rendered << current_line + current_line = '' + end + end + end + + # Add leftover text + rendered << current_line if current_line.present? + + num_lines = rendered.size + numbers = (@linenostart..num_lines + @linenostart - 1).to_a + + { numbers: numbers, code: rendered } + end + + def wrap_linenos(numbers) + if @anchorlinenos + numbers.map! do |number| + "<a href=\"##{@lineanchorsid}#{number}\">#{number}</a>" + end + end + numbers.join("\n") + end + + def wrap_lines(lines) + if @lineanchors + lines = lines.each_with_index.map do |line, index| + number = index + @linenostart + + if @linenos == 'inline' + "<a name=\"L#{number}\"></a>" \ + "<span class=\"linenos\">#{number}</span>" \ + "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \ + '</span>' + else + "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \ + '</span>' + end + end + lines.join("\n") + else + if @linenos == 'inline' + lines = lines.each_with_index.map do |line, index| + number = index + @linenostart + "<span class=\"linenos\">#{number}</span>#{line}" + end + lines.join("\n") + else + lines.join("\n") + end + end + end + + def span(tok, val) + # http://stackoverflow.com/a/1600584/2587286 + val = CGI.escapeHTML(val) + + if tok.shortname.empty? + val + else + if @inline_theme + rules = @inline_theme.style_for(tok).rendered_rules + "<span style=\"#{rules.to_a.join(';')}\"#{val}</span>" + else + "<span class=\"#{tok.shortname}\">#{val}</span>" + end + end + end + end + end +end diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 946902e2f6d..a3455728a94 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -41,7 +41,7 @@ shell_path="/bin/bash" 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 +if [ `whoami` != "$app_user" ]; then eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit; fi diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index edb987875df..efa0898900f 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -38,6 +38,11 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } +## Experimental: gitlab-git-http-server +# upstream gitlab-git-http-server { +# server localhost:8181; +# } + ## Normal HTTP host server { ## Either remove "default_server" from the listen line below, @@ -109,6 +114,26 @@ server { proxy_pass http://gitlab; } + ## Experimental: send Git HTTP traffic to gitlab-git-http-server instead of Unicorn + # location ~ [-\/\w\.]+\.git\/ { + # ## If you use HTTPS make sure you disable gzip compression + # ## to be safe against BREACH attack. + # # gzip off; + + # ## https://github.com/gitlabhq/gitlabhq/issues/694 + # ## Some requests take more than 30 seconds. + # proxy_read_timeout 300; + # proxy_connect_timeout 300; + # proxy_redirect off; + + # proxy_set_header Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + + # proxy_pass http://gitlab-git-http-server; + # } + ## Enable gzip compression as per rails guide: ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression ## WARNING: If you are using relative urls remove the block below diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 766559b49f6..314525518f1 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -42,6 +42,11 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } +## Experimental: gitlab-git-http-server +# upstream gitlab-git-http-server { +# server localhost:8181; +# } + ## Redirects all HTTP traffic to the HTTPS host server { ## Either remove "default_server" from the listen line below, @@ -156,6 +161,26 @@ server { proxy_pass http://gitlab; } + ## Experimental: send Git HTTP traffic to gitlab-git-http-server instead of Unicorn + # location ~ [-\/\w\.]+\.git\/ { + # ## If you use HTTPS make sure you disable gzip compression + # ## to be safe against BREACH attack. + # gzip off; + + # ## https://github.com/gitlabhq/gitlabhq/issues/694 + # ## Some requests take more than 30 seconds. + # proxy_read_timeout 300; + # proxy_connect_timeout 300; + # proxy_redirect off; + + # proxy_set_header Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-Ssl on; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_pass http://gitlab-git-http-server; + # } + ## Enable gzip compression as per rails guide: ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression ## WARNING: If you are using relative urls remove the block below diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index aed84226a2f..d19695f92fb 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -485,7 +485,8 @@ namespace :gitlab do if project.empty_repo? puts "repository is empty".magenta - elsif File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path) + elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && + (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) puts 'ok'.green else puts "wrong or missing hooks".red @@ -748,13 +749,13 @@ namespace :gitlab do end def check_ruby_version - required_version = Gitlab::VersionInfo.new(2, 0, 0) + required_version = Gitlab::VersionInfo.new(2, 1, 0) current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version))) print "Ruby version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".green else puts "no".red try_fixing_it( @@ -772,7 +773,7 @@ namespace :gitlab do print "Git version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".green else puts "no".red try_fixing_it( @@ -806,4 +807,3 @@ namespace :gitlab do end end end - diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 5f83e5e8e7f..c1ee271ae2b 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -62,11 +62,11 @@ namespace :gitlab do project = Projects::CreateService.new(user, project_params).execute - if project.valid? + if project.persisted? puts " * Created #{project.name} (#{repo_path})".green else puts " * Failed trying to create #{project.name} (#{repo_path})".red - puts " Validation Errors: #{project.errors.messages}".red + puts " Errors: #{project.errors.messages}".red end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index bf221f06d3b..d6883a563ee 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -30,8 +30,7 @@ namespace :gitlab do # check database adapter database_adapter = ActiveRecord::Base.connection.adapter_name.downcase - project = Project.new(path: "some-project") - project.path = "some-project" + project = Group.new(path: "some-group").projects.build(path: "some-project") # construct clone URLs http_clone_url = project.http_url_to_repo ssh_clone_url = project.ssh_url_to_repo diff --git a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake deleted file mode 100644 index 102c6ae55d5..00000000000 --- a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake +++ /dev/null @@ -1,73 +0,0 @@ -require "#{Rails.root}/app/helpers/emails_helper" -require 'action_view/helpers' -extend ActionView::Helpers - -include ActionView::Context -include EmailsHelper - -namespace :gitlab do - desc "Email google whitelisting email with example email for actions in inbox" - task mail_google_schema_whitelisting: :environment do - subject = "Rails | Implemented feature" - url = "#{Gitlab.config.gitlab.url}/base/rails-project/issues/#{rand(1..100)}#note_#{rand(10..1000)}" - schema = email_action(url) - body = email_template(schema, url) - mail = Notify.test_email("schema.whitelisting+sample@gmail.com", subject, body.html_safe) - if send_now - mail.deliver - else - puts "WOULD SEND:" - end - puts mail - end - - def email_template(schema, url) - "<html lang='en'> - <head> - <meta content='text/html; charset=utf-8' http-equiv='Content-Type'> - <title> - GitLab - </title> - </meta> - </head> - <style> - img { - max-width: 100%; - height: auto; - } - p.details { - font-style:italic; - color:#777 - } - .footer p { - font-size:small; - color:#777 - } - </style> - <body> - <div class='content'> - <div> - <p>I like it :+1: </p> - </div> - </div> - - <div class='footer' style='margin-top: 10px;'> - <p> - <br> - <a href=\"#{url}\">View it on GitLab</a> - You're receiving this notification because you are a member of the Base / Rails Project project team. - #{schema} - </p> - </div> - </body> - </html>" - end - - def send_now - if ENV['SEND'] == "true" - true - else - false - end - end -end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake new file mode 100644 index 00000000000..9b636f12d9f --- /dev/null +++ b/lib/tasks/gitlab/update_commit_count.rake @@ -0,0 +1,20 @@ +namespace :gitlab do + desc "GitLab | Update commit count for projects" + task update_commit_count: :environment do + projects = Project.where(commit_count: 0) + puts "#{projects.size} projects need to be updated. This might take a while." + ask_to_continue unless ENV['force'] == 'yes' + + projects.find_each(batch_size: 100) do |project| + print "#{project.name_with_namespace.yellow} ... " + + unless project.repo_exists? + puts "skipping, because the repo is empty".magenta + next + end + + project.update_commit_count + puts project.commit_count.to_s.green + end + end +end diff --git a/lib/unfold_form.rb b/lib/unfold_form.rb index 46b12beeaaf..fcd01503d1b 100644 --- a/lib/unfold_form.rb +++ b/lib/unfold_form.rb @@ -8,4 +8,5 @@ class UnfoldForm attribute :bottom, Boolean attribute :unfold, Boolean, default: true attribute :offset, Integer + attribute :indent, Integer, default: 0 end diff --git a/public/robots.txt b/public/robots.txt index 085187fa58b..528f421083e 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,5 +1,66 @@ -# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / + +User-Agent: * + +# Add a 1 second delay between successive requests to the same server, limits resources used by crawler +# Only some crawlers respect this setting, e.g. Googlebot does not +# Crawl-delay: 1 + +# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application +Disallow: /autocomplete/users +Disallow: /search +Disallow: /api +Disallow: /admin +Disallow: /profile +Disallow: /dashboard +Disallow: /projects/new +Disallow: /groups/new +Disallow: /groups/*/edit +Disallow: /users + +# Global snippets +Disallow: /s +Disallow: /snippets/new +Disallow: /snippets/*/edit +Disallow: /snippets/*/raw + +# Project details +Disallow: /*/*.git +Disallow: /*/*/fork/new +Disallow: /*/*/repository/archive* +Disallow: /*/*/activity +Disallow: /*/*/new +Disallow: /*/*/edit +Disallow: /*/*/raw +Disallow: /*/*/blame +Disallow: /*/*/commits/*/* +Disallow: /*/*/commit +Disallow: /*/*/compare +Disallow: /*/*/branches/new +Disallow: /*/*/tags/new +Disallow: /*/*/network +Disallow: /*/*/graphs +Disallow: /*/*/milestones/new +Disallow: /*/*/milestones/*/edit +Disallow: /*/*/issues/new +Disallow: /*/*/issues/*/edit +Disallow: /*/*/merge_requests/new +Disallow: /*/*/merge_requests/*.patch +Disallow: /*/*/merge_requests/*.diff +Disallow: /*/*/merge_requests/*/edit +Disallow: /*/*/merge_requests/*/diffs +Disallow: /*/*/project_members/import +Disallow: /*/*/labels/new +Disallow: /*/*/labels/*/edit +Disallow: /*/*/wikis/*/edit +Disallow: /*/*/snippets/new +Disallow: /*/*/snippets/*/edit +Disallow: /*/*/snippets/*/raw +Disallow: /*/*/deploy_keys +Disallow: /*/*/hooks +Disallow: /*/*/services +Disallow: /*/*/protected_branches diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 550a91a79e2..c40b2c2a583 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -36,4 +36,46 @@ describe Admin::UsersController do expect(user.access_locked?).to be_falsey end end + + describe 'PUT confirm/:id' do + let(:user) { create(:user, confirmed_at: nil) } + + before do + request.env["HTTP_REFERER"] = "/" + end + + it 'confirms user' do + put :confirm, id: user.username + user.reload + expect(user.confirmed?).to be_truthy + end + end + + describe 'PATCH disable_two_factor' do + let(:user) { create(:user) } + + it 'disables 2FA for the user' do + expect(user).to receive(:disable_two_factor!) + allow(subject).to receive(:user).and_return(user) + + go + end + + it 'redirects back' do + go + + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'displays an alert' do + go + + expect(flash[:notice]). + to eq 'Two-factor Authentication has been disabled for this user' + end + + def go + patch :disable_two_factor, id: user.to_param + end + end end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 9ad9cb41cc1..3521d690259 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -4,20 +4,33 @@ describe AutocompleteController do let!(:project) { create(:project) } let!(:user) { create(:user) } let!(:user2) { create(:user) } + let!(:non_member) { create(:user) } context 'project members' do before do sign_in(user) project.team << [user, :master] - - get(:users, project_id: project.id) end let(:body) { JSON.parse(response.body) } - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 1 } - it { expect(body.first["username"]).to eq user.username } + describe 'GET #users with project ID' do + before do + get(:users, project_id: project.id) + end + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 1 } + it { expect(body.first["username"]).to eq user.username } + end + + describe 'GET #users with unknown project' do + before do + get(:users, project_id: 'unknown') + end + + it { expect(response.status).to eq(404) } + end end context 'group members' do @@ -26,15 +39,48 @@ describe AutocompleteController do before do sign_in(user) group.add_owner(user) + end + + let(:body) { JSON.parse(response.body) } + + describe 'GET #users with group ID' do + before do + get(:users, group_id: group.id) + end - get(:users, group_id: group.id) + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 1 } + it { expect(body.first["username"]).to eq user.username } + end + + describe 'GET #users with unknown group ID' do + before do + get(:users, group_id: 'unknown') + end + + it { expect(response.status).to eq(404) } + end + end + + context 'non-member login for public project' do + let!(:project) { create(:project, :public) } + + before do + sign_in(non_member) + project.team << [user, :master] end let(:body) { JSON.parse(response.body) } - it { expect(body).to be_kind_of(Array) } - it { expect(body.size).to eq 1 } - it { expect(body.first["username"]).to eq user.username } + describe 'GET #users with project ID' do + before do + get(:users, project_id: project.id) + end + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 2 } + it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) } + end end context 'all users' do @@ -48,4 +94,52 @@ describe AutocompleteController do it { expect(body).to be_kind_of(Array) } it { expect(body.size).to eq User.count } end + + context 'unauthenticated user' do + let(:public_project) { create(:project, :public) } + let(:body) { JSON.parse(response.body) } + + describe 'GET #users with public project' do + before do + public_project.team << [user, :guest] + get(:users, project_id: public_project.id) + end + + it { expect(body).to be_kind_of(Array) } + it { expect(body.size).to eq 1 } + end + + describe 'GET #users with project' do + before do + get(:users, project_id: project.id) + end + + it { expect(response.status).to eq(302) } + end + + describe 'GET #users with unknown project' do + before do + get(:users, project_id: 'unknown') + end + + it { expect(response.status).to eq(302) } + end + + describe 'GET #users with inaccessible group' do + before do + project.team << [user, :guest] + get(:users, group_id: user.namespace.id) + end + + it { expect(response.status).to eq(302) } + end + + describe 'GET #users with no project' do + before do + get(:users) + end + + it { expect(response.status).to eq(302) } + end + end end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb index bd4c946b64b..8e06d4bdc77 100644 --- a/spec/controllers/branches_controller_spec.rb +++ b/spec/controllers/branches_controller_spec.rb @@ -54,6 +54,13 @@ describe Projects::BranchesController do let(:ref) { "<script>alert('ref');</script>" } it { is_expected.to render_template('new') } end + + context "valid branch name with encoded slashes" do + let(:branch) { "feature%2Ftest" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + it { project.repository.branch_names.include?('feature/test')} + end end describe "POST destroy" do @@ -74,6 +81,19 @@ describe Projects::BranchesController do it { expect(subject).to render_template('destroy') } end + context "valid branch name with unencoded slashes" do + let(:branch) { "improve/awesome" } + + it { expect(response.status).to eq(200) } + it { expect(subject).to render_template('destroy') } + end + + context "valid branch name with encoded slashes" do + let(:branch) { "improve%2Fawesome" } + + it { expect(response.status).to eq(200) } + it { expect(subject).to render_template('destroy') } + end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index d5d9310e603..89e595121a7 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -39,12 +39,14 @@ describe Import::BitbucketController do it "assigns variables" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id) - stub_client(projects: [@repo]) + client = stub_client(projects: [@repo]) + allow(client).to receive(:incompatible_projects).and_return([]) get :status expect(assigns(:already_added_projects)).to eq([@project]) expect(assigns(:repos)).to eq([@repo]) + expect(assigns(:incompatible_repos)).to eq([]) end it "does not show already added project" do diff --git a/spec/controllers/profile_keys_controller_spec.rb b/spec/controllers/profile_keys_controller_spec.rb index 593d3e9eb56..b6573f105dc 100644 --- a/spec/controllers/profile_keys_controller_spec.rb +++ b/spec/controllers/profile_keys_controller_spec.rb @@ -48,6 +48,17 @@ describe Profiles::KeysController do expect(response.body).not_to eq("") expect(response.body).to eq(user.all_ssh_keys.join("\n")) + + # Unique part of key 1 + expect(response.body).to match(/PWx6WM4lhHNedGfBpPJNPpZ/) + # Key 2 + expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/) + end + + it "should not render the comment of the key" do + get :get_keys, username: user.username + + expect(response.body).not_to match(/dummy@gitlab.com/) end it "should respond with text/plain content type" do diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index aa09f1a758d..f54706e3aa3 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -105,19 +105,12 @@ describe Profiles::TwoFactorAuthsController do end describe 'DELETE destroy' do - let(:user) { create(:user, :two_factor) } - let!(:codes) { user.generate_otp_backup_codes! } + let(:user) { create(:user, :two_factor) } - it 'clears all 2FA-related fields' do - expect(user).to be_two_factor_enabled - expect(user.otp_backup_codes).not_to be_nil - expect(user.encrypted_otp_secret).not_to be_nil + it 'disables two factor' do + expect(user).to receive(:disable_two_factor!) delete :destroy - - expect(user).not_to be_two_factor_enabled - expect(user.otp_backup_codes).to be_nil - expect(user.encrypted_otp_secret).to be_nil end it 'redirects to profile_account_path' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb new file mode 100644 index 00000000000..871b9219ca9 --- /dev/null +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -0,0 +1,37 @@ +require('spec_helper') + +describe Projects::IssuesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + + before do + sign_in(user) + project.team << [user, :developer] + controller.instance_variable_set(:@project, project) + end + + describe "GET #index" do + it "returns index" do + get :index, namespace_id: project.namespace.id, project_id: project.id + + expect(response.status).to eq(200) + end + + it "returns 404 when issues are disabled" do + project.issues_enabled = false + project.save + + get :index, namespace_id: project.namespace.id, project_id: project.id + expect(response.status).to eq(404) + end + + it "returns 404 when external issue tracker is enabled" do + allow(project).to receive(:default_issues_tracker?).and_return(false) + + get :index, namespace_id: project.namespace.id, project_id: project.id + expect(response.status).to eq(404) + end + + end +end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb new file mode 100644 index 00000000000..d3868c13202 --- /dev/null +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::MilestonesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project, milestone: milestone) } + + before do + sign_in(user) + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "#destroy" do + it "should remove milestone" do + expect(issue.milestone_id).to eq(milestone.id) + delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js + expect(response).to be_success + expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound) + issue.reload + expect(issue.milestone_id).to eq(nil) + # Check system note left for milestone removal + last_note = project.issues.find(issue.id).notes[-1].note + expect(last_note).to eq('Milestone removed') + end + end +end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb new file mode 100644 index 00000000000..d4ecd98e12d --- /dev/null +++ b/spec/controllers/projects/services_controller_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Projects::ServicesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { create(:service, project: project) } + + before do + sign_in(user) + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + controller.instance_variable_set(:@service, service) + request.env["HTTP_REFERER"] = "/" + end + + describe "#test" do + context 'success' do + it "should redirect and show success message" do + expect(service).to receive(:test).and_return({ success: true, result: 'done' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:notice]).to eq('We sent a request to the provided URL') + end + end + + context 'failure' do + it "should redirect and show failure message" do + expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') + end + end + end +end diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index e09caf5df13..53915856357 100644 --- a/spec/controllers/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -8,9 +8,6 @@ describe Projects::TreeController do sign_in(user) project.team << [user, :master] - - allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) - allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) controller.instance_variable_set(:@project, project) end @@ -44,6 +41,32 @@ describe Projects::TreeController do let(:id) { 'invalid-branch/encoding/' } it { is_expected.to respond_with(:not_found) } end + + context "valid empty branch, invalid path" do + let(:id) { 'empty-branch/invalid-path/' } + it { is_expected.to respond_with(:not_found) } + end + + context "valid empty branch" do + let(:id) { 'empty-branch' } + it { is_expected.to respond_with(:success) } + end + + context "invalid SHA commit ID" do + let(:id) { 'ff39438/.gitignore' } + it { is_expected.to respond_with(:not_found) } + end + + context "valid SHA commit ID" do + let(:id) { '6d39438' } + it { is_expected.to respond_with(:success) } + end + + context "valid SHA commit ID with path" do + let(:id) { '6d39438/.gitignore' } + it { expect(response.status).to eq(302) } + end + end describe 'GET show with blob path' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index d47a37914df..9f89101d7f7 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1,25 +1,38 @@ require 'spec_helper' describe UsersController do - let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } - - before do - sign_in(user) - end + let(:user) { create(:user) } describe 'GET #show' do - render_views + it 'is case-insensitive' do + user = create(:user, username: 'CamelCaseUser') + sign_in(user) + + get :show, username: user.username.downcase - it 'renders the show template' do - get :show, username: user.username - expect(response.status).to eq(200) - expect(response).to render_template('show') + expect(response).to be_success + end + + context 'with rendered views' do + render_views + + it 'renders the show template' do + sign_in(user) + + get :show, username: user.username + + expect(response).to be_success + expect(response).to render_template('show') + end end end describe 'GET #calendar' do it 'renders calendar' do + sign_in(user) + get :calendar, username: user.username + expect(response).to render_template('calendar') end end @@ -30,6 +43,8 @@ describe UsersController do before do allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) + + sign_in(user) project.team << [user, :developer] end diff --git a/spec/factories.rb b/spec/factories.rb index 578a2e4dc69..200f18f660d 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -32,6 +32,7 @@ FactoryGirl.define do before(:create) do |user| user.two_factor_enabled = true user.otp_secret = User.generate_otp_secret(32) + user.generate_otp_backup_codes! end end @@ -99,7 +100,7 @@ FactoryGirl.define do factory :key do title key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" end factory :deploy_key, class: 'DeployKey' do diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb new file mode 100644 index 00000000000..29fcbc5e197 --- /dev/null +++ b/spec/factories/abuse_reports.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :abuse_report do + reporter factory: :user + user + message 'User sends spam' + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 102678a1d74..1d500a11ad7 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # FactoryGirl.define do diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb new file mode 100644 index 00000000000..71be66303d2 --- /dev/null +++ b/spec/features/admin/admin_disables_two_factor_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +feature 'Admin disables 2FA for a user', feature: true do + scenario 'successfully', js: true do + login_as(:admin) + user = create(:user, :two_factor) + + edit_user(user) + page.within('.two-factor-status') do + click_link 'Disable' + end + + page.within('.two-factor-status') do + expect(page).to have_content 'Disabled' + expect(page).not_to have_button 'Disable' + end + end + + scenario 'for a user without 2FA enabled' do + login_as(:admin) + user = create(:user) + + edit_user(user) + + page.within('.two-factor-status') do + expect(page).not_to have_button 'Disable' + end + end + + def edit_user(user) + visit admin_user_path(user) + end +end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index edc1c63a0aa..891df65216d 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Group' do +feature 'Group', feature: true do describe 'description' do let(:group) { create(:group) } let(:path) { group_path(group) } diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb new file mode 100644 index 00000000000..f600f8684ac --- /dev/null +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +feature 'Issue filtering by Milestone', feature: true do + include Select2Helper + + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + + scenario 'filters by no Milestone', js: true do + create(:issue, project: project) + create(:issue, project: project, milestone: milestone) + + visit_issues(project) + filter_by_milestone(Milestone::None.title) + + expect(page).to have_css('.issue-title', count: 1) + end + + scenario 'filters by a specific Milestone', js: true do + create(:issue, project: project, milestone: milestone) + create(:issue, project: project) + + visit_issues(project) + filter_by_milestone(milestone.title) + + expect(page).to have_css('.issue-title', count: 1) + end + + def visit_issues(project) + visit namespace_project_issues_path(project.namespace, project) + end + + def filter_by_milestone(title) + select2(title, from: '#milestone_title') + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 808a6eeb958..32fd4065bb4 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -92,22 +92,6 @@ describe 'Issues', feature: true do let(:issue) { @issue } - it 'should allow filtering by issues with no specified milestone' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: IssuableFinder::NONE) - - expect(page).not_to have_content 'foobar' - expect(page).to have_content 'barbaz' - expect(page).to have_content 'gitlab' - end - - it 'should allow filtering by a specified milestone' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: issue.milestone.title) - - expect(page).to have_content 'foobar' - expect(page).not_to have_content 'barbaz' - expect(page).not_to have_content 'gitlab' - end - it 'should allow filtering by issues with no specified assignee' do visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE) diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 046a9f6191d..cef432e512b 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Login' do +feature 'Login', feature: true do describe 'with two-factor authentication' do context 'with valid username/password' do let(:user) { create(:user, :two_factor) } diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 902968cebcb..3da4dfc2b23 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -15,412 +15,217 @@ require 'erb' # -> `markdown` helper # -> Redcarpet::Render::GitlabHTML converts Markdown to HTML # -> Post-process HTML -# -> `gfm_with_options` helper +# -> `gfm` helper # -> HTML::Pipeline -# -> Sanitize -# -> RelativeLink -# -> Emoji -# -> Table of Contents -# -> Autolinks -# -> Rinku (http, https, ftp) -# -> Other schemes -# -> ExternalLink -# -> References -# -> TaskList +# -> SanitizationFilter +# -> Other filters, depending on pipeline # -> `html_safe` # -> Template # # See the MarkdownFeature class for setup details. -describe 'GitLab Markdown' do - include ActionView::Helpers::TagHelper - include ActionView::Helpers::UrlHelper +describe 'GitLab Markdown', feature: true do include Capybara::Node::Matchers include GitlabMarkdownHelper + include MarkdownMatchers - # `markdown` calls these two methods - def current_user - @feat.user - end - - def user_color_scheme_class - :white - end - - # Let's only parse this thing once - before(:all) do - @feat = MarkdownFeature.new - - # `markdown` expects a `@project` variable - @project = @feat.project - - @md = markdown(@feat.raw_markdown) - @doc = Nokogiri::HTML::DocumentFragment.parse(@md) - end - - after(:all) do - @feat.teardown + # Sometimes it can be useful to see the parsed output of the Markdown document + # for debugging. Call this method to write the output to + # `tmp/capybara/<filename>.html`. + def write_markdown(filename = 'markdown_spec') + File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file| + file.puts @html + end end - # 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 + def doc(html = @html) + Nokogiri::HTML::DocumentFragment.parse(html) 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 - # end - # end - - describe 'Markdown' do - describe 'No Intra Emphasis' do + # Shared behavior that all pipelines should exhibit + shared_examples 'all pipelines' do + describe 'Redcarpet extensions' do it 'does not parse emphasis inside of words' do - body = get_section('no-intra-emphasis') - expect(body.to_html).not_to match('foo<em>bar</em>baz') + expect(doc.to_html).not_to match('foo<em>bar</em>baz') end - end - describe 'Tables' do it 'parses table Markdown' do - body = get_section('tables') - expect(body).to have_selector('th:contains("Header")') - expect(body).to have_selector('th:contains("Row")') - expect(body).to have_selector('th:contains("Example")') + aggregate_failures do + expect(doc).to have_selector('th:contains("Header")') + expect(doc).to have_selector('th:contains("Row")') + expect(doc).to have_selector('th:contains("Example")') + end end it 'allows Markdown in tables' do - expect(@doc.at_css('td:contains("Baz")').children.to_html). + expect(doc.at_css('td:contains("Baz")').children.to_html). to eq '<strong>Baz</strong>' end - end - describe 'Fenced Code Blocks' do it 'parses fenced code blocks' do - expect(@doc).to have_selector('pre.code.highlight.white.c') - expect(@doc).to have_selector('pre.code.highlight.white.python') + aggregate_failures do + expect(doc).to have_selector('pre.code.highlight.white.c') + expect(doc).to have_selector('pre.code.highlight.white.python') + end end - end - describe 'Strikethrough' do it 'parses strikethroughs' do - expect(@doc).to have_selector(%{del:contains("and this text doesn't")}) + expect(doc).to have_selector(%{del:contains("and this text doesn't")}) end - end - describe 'Superscript' do it 'parses superscript' do - body = get_section('superscript') - expect(body.to_html).to match('1<sup>st</sup>') - expect(body.to_html).to match('2<sup>nd</sup>') + expect(doc).to have_selector('sup', count: 2) end end - end - describe 'HTML::Pipeline' do describe 'SanitizationFilter' do - it 'uses a permissive whitelist' do - 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[src*="smile.png"]') - expect(@doc).to have_selector('br') - expect(@doc).to have_selector('hr') + it 'permits b elements' do + expect(doc).to have_selector('b:contains("b tag")') end - it 'permits span elements' do - expect(@doc).to have_selector('span:contains("span tag")') + it 'permits em elements' do + expect(doc).to have_selector('em:contains("em tag")') end - it 'permits table alignment' do - expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' - expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' - expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' - - expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' - expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' - expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' + it 'permits code elements' do + expect(doc).to have_selector('code:contains("code tag")') end - it 'removes `rel` attribute from links' do - body = get_section('sanitizationfilter') - expect(body).not_to have_selector('a[rel="bookmark"]') + it 'permits kbd elements' do + expect(doc).to have_selector('kbd:contains("s")') end - it "removes `href` from `a` elements if it's fishy" do - expect(@doc).not_to have_selector('a[href*="javascript"]') + it 'permits strike elements' do + expect(doc).to have_selector('strike:contains(Emoji)') end - end - describe 'Escaping' do - let(:table) { @doc.css('table').last.at_css('tbody') } - - it 'escapes non-tag angle brackets' do - expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' + it 'permits img elements' do + expect(doc).to have_selector('img[src*="smile.png"]') end - end - - describe 'Edge Cases' do - it 'allows markup inside link elements' do - expect(@doc.at_css('a[href="#link-emphasis"]').to_html). - to eq %{<a href="#link-emphasis"><em>text</em></a>} - - expect(@doc.at_css('a[href="#link-strong"]').to_html). - to eq %{<a href="#link-strong"><strong>text</strong></a>} - expect(@doc.at_css('a[href="#link-code"]').to_html). - to eq %{<a href="#link-code"><code>text</code></a>} + it 'permits br elements' do + expect(doc).to have_selector('br') end - end - describe 'EmojiFilter' do - it 'parses Emoji' do - expect(@doc).to have_selector('img.emoji', count: 10) + it 'permits hr elements' do + expect(doc).to have_selector('hr') end - end - describe 'TableOfContentsFilter' do - it 'creates anchors inside header elements' do - expect(@doc).to have_selector('h1 a#gitlab-markdown') - expect(@doc).to have_selector('h2 a#markdown') - expect(@doc).to have_selector('h3 a#autolinkfilter') + it 'permits span elements' do + expect(doc).to have_selector('span:contains("span tag")') end - end - describe 'AutolinkFilter' do - let(:list) { get_section('autolinkfilter').next_element } - - def item(index) - list.at_css("li:nth-child(#{index})") + it 'permits style attribute in th elements' do + aggregate_failures do + expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' + expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' + expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' + end end - it 'autolinks http://' do - expect(item(1).children.first.name).to eq 'a' - expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/' + it 'permits style attribute in td elements' do + aggregate_failures do + expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' + expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' + expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' + end end - it 'autolinks https://' do - expect(item(2).children.first.name).to eq 'a' - expect(item(2).children.first['href']).to eq 'https://google.com/' + it 'removes `rel` attribute from links' do + expect(doc).not_to have_selector('a[rel="bookmark"]') end - it 'autolinks ftp://' do - expect(item(3).children.first.name).to eq 'a' - expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/' + it "removes `href` from `a` elements if it's fishy" do + expect(doc).not_to have_selector('a[href*="javascript"]') end + end - it 'autolinks smb://' do - expect(item(4).children.first.name).to eq 'a' - expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz' + describe 'Escaping' do + it 'escapes non-tag angle brackets' do + table = doc.css('table').last.at_css('tbody') + expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' end + end - it 'autolinks irc://' do - expect(item(5).children.first.name).to eq 'a' - expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git' - end + describe 'Edge Cases' do + it 'allows markup inside link elements' do + aggregate_failures do + expect(doc.at_css('a[href="#link-emphasis"]').to_html). + to eq %{<a href="#link-emphasis"><em>text</em></a>} - it 'autolinks short, invalid URLs' do - expect(item(6).children.first.name).to eq 'a' - expect(item(6).children.first['href']).to eq 'http://localhost:3000' - end + expect(doc.at_css('a[href="#link-strong"]').to_html). + to eq %{<a href="#link-strong"><strong>text</strong></a>} - %w(code a kbd).each do |elem| - it "ignores links inside '#{elem}' element" do - body = get_section('autolinkfilter') - expect(body).not_to have_selector("#{elem} a") + expect(doc.at_css('a[href="#link-code"]').to_html). + to eq %{<a href="#link-code"><code>text</code></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' + link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to match 'nofollow' end it 'ignores internal link' do - expect(links.css('a').last.to_html).not_to match 'nofollow' + link = doc.at_css('a:contains("GitLab Root")') + expect(link.attr('rel')).not_to match 'nofollow' end end + end - describe 'ReferenceFilter' do - it 'handles references in headers' do - header = @doc.at_css('#reference-filters-eg-1').parent - - expect(header.css('a').size).to eq 2 - end - - it "handles references in Markdown" do - body = get_section('reference-filters-eg-1') - expect(body).to have_selector('em a.gfm-merge_request', count: 1) - end - - it 'parses user references' do - body = get_section('userreferencefilter') - expect(body).to have_selector('a.gfm.gfm-project_member', count: 3) - end - - it 'parses issue references' do - body = get_section('issuereferencefilter') - expect(body).to have_selector('a.gfm.gfm-issue', count: 2) - end - - it 'parses merge request references' do - body = get_section('mergerequestreferencefilter') - expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2) - end + context 'default pipeline' do + before(:all) do + @feat = MarkdownFeature.new - it 'parses snippet references' do - body = get_section('snippetreferencefilter') - expect(body).to have_selector('a.gfm.gfm-snippet', count: 2) - end + # `gfm` helper depends on a `@project` variable + @project = @feat.project - it 'parses commit range references' do - body = get_section('commitrangereferencefilter') - expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2) - end + @html = markdown(@feat.raw_markdown) + end - it 'parses commit references' do - body = get_section('commitreferencefilter') - expect(body).to have_selector('a.gfm.gfm-commit', count: 2) - end + it_behaves_like 'all pipelines' - it 'parses label references' do - body = get_section('labelreferencefilter') - expect(body).to have_selector('a.gfm.gfm-label', count: 3) - end + it 'includes RelativeLinkFilter' do + expect(doc).to parse_relative_links end - describe 'Task Lists' do - it 'generates task lists' do - body = get_section('task-lists') - expect(body).to have_selector('ul.task-list', count: 2) - expect(body).to have_selector('li.task-list-item', count: 7) - expect(body).to have_selector('input[checked]', count: 3) - end + it 'includes EmojiFilter' do + expect(doc).to parse_emoji end - end -end - -# This is a helper class used by the GitLab Markdown feature spec -# -# Because the feature spec only cares about the output of the Markdown, and the -# test setup and teardown and parsing is fairly expensive, we only want to do it -# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` -# block, so we fake it by encapsulating all the shared setup in this class. -# -# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for -# reference to the factory-created objects. -class MarkdownFeature - include FactoryGirl::Syntax::Methods - - def initialize - DatabaseCleaner.start - end - - def teardown - DatabaseCleaner.clean - end - - def user - @user ||= create(:user) - end - def group - unless @group - @group = create(:group) - @group.add_user(user, Gitlab::Access::DEVELOPER) + it 'includes TableOfContentsFilter' do + expect(doc).to create_header_links end - @group - end - - # Direct references ---------------------------------------------------------- - - def project - @project ||= create(:project) - end - - def issue - @issue ||= create(:issue, project: project) - end - - def merge_request - @merge_request ||= create(:merge_request, :simple, source_project: project) - end - - def snippet - @snippet ||= create(:project_snippet, project: project) - end - - def commit - @commit ||= project.commit - end - - def commit_range - unless @commit_range - commit2 = project.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + it 'includes AutolinkFilter' do + expect(doc).to create_autolinks end - @commit_range - end - - def simple_label - @simple_label ||= create(:label, name: 'gfm', project: project) - end - - def label - @label ||= create(:label, name: 'awaiting feedback', project: project) - end - - # Cross-references ----------------------------------------------------------- - - def xproject - unless @xproject - namespace = create(:namespace, name: 'cross-reference') - @xproject = create(:project, namespace: namespace) - @xproject.team << [user, :developer] + it 'includes all reference filters' do + aggregate_failures do + expect(doc).to reference_users + expect(doc).to reference_issues + expect(doc).to reference_merge_requests + expect(doc).to reference_snippets + expect(doc).to reference_commit_ranges + expect(doc).to reference_commits + expect(doc).to reference_labels + end end - @xproject - end - - def xissue - @xissue ||= create(:issue, project: xproject) - end - - def xmerge_request - @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) - end - - def xsnippet - @xsnippet ||= create(:project_snippet, project: xproject) - end - - def xcommit - @xcommit ||= xproject.commit - end - - def xcommit_range - unless @xcommit_range - xcommit2 = xproject.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + it 'includes TaskListFilter' do + expect(doc).to parse_task_lists end + end - @xcommit_range + # `markdown` calls these two methods + def current_user + @feat.user end - def raw_markdown - fixture = Rails.root.join('spec/fixtures/markdown.md.erb') - ERB.new(File.read(fixture)).result(binding) + def user_color_scheme_class + :white end end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb new file mode 100644 index 00000000000..f70214e1122 --- /dev/null +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +feature 'Merge Request filtering by Milestone', feature: true do + include Select2Helper + + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + + scenario 'filters by no Milestone', js: true do + create(:merge_request, :with_diffs, source_project: project) + create(:merge_request, :simple, source_project: project, milestone: milestone) + + visit_merge_requests(project) + filter_by_milestone(Milestone::None.title) + + expect(page).to have_css('.merge-request-title', count: 1) + end + + scenario 'filters by a specific Milestone', js: true do + create(:merge_request, :with_diffs, source_project: project, milestone: milestone) + create(:merge_request, :simple, source_project: project) + + visit_merge_requests(project) + filter_by_milestone(milestone.title) + + expect(page).to have_css('.merge-request-title', count: 1) + end + + def visit_merge_requests(project) + visit namespace_project_merge_requests_path(project.namespace, project) + end + + def filter_by_milestone(title) + select2(title, from: '#milestone_title') + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index ad37b589b84..d7cb3b2e86e 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Comments' do +describe 'Comments', feature: true do include RepoHelpers describe 'On a merge request', js: true, feature: true do diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb index a34efce09ef..2b6311e4fd7 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Password reset' do +feature 'Password reset', feature: true do def forgot_password click_on 'Forgot your password?' fill_in 'Email', with: user.email diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 9fe2e610555..c80253fead8 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -9,8 +9,7 @@ describe 'Profile account page', feature: true do describe 'when signup is enabled' do before do - allow_any_instance_of(ApplicationSetting). - to receive(:signup_enabled?).and_return(true) + stub_application_setting(signup_enabled: true) visit profile_account_path end @@ -24,8 +23,7 @@ describe 'Profile account page', feature: true do describe 'when signup is disabled' do before do - allow_any_instance_of(ApplicationSetting). - to receive(:signup_enabled?).and_return(false) + stub_application_setting(signup_enabled: false) visit profile_account_path end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index 03e78c533db..9bc6145dda4 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Profile > Preferences' do +describe 'Profile > Preferences', feature: true do let(:user) { create(:user) } before do diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index f8eea70ec4a..a362c54b9ad 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Project' do +feature 'Project', feature: true do describe 'description' do let(:project) { create(:project) } let(:path) { namespace_project_path(project.namespace, project) } @@ -22,9 +22,9 @@ feature 'Project' do end it 'sanitizes unwanted tags' do - project.update_attribute(:description, '# Project Description') + project.update_attribute(:description, "```\ncode\n```") visit path - expect(page).not_to have_css('.project-home-desc h1') + expect(page).not_to have_css('.project-home-desc code') end it 'permits `rel` attribute on links' do diff --git a/spec/features/admin/security_spec.rb b/spec/features/security/admin_access_spec.rb index 175fa9d4647..fe8cd7b7602 100644 --- a/spec/features/admin/security_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Admin::Projects", feature: true do + include AccessMatchers + describe "GET /admin/projects" do subject { admin_namespaces_projects_path } diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 67238e3ab76..c38cddbb904 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Dashboard access", feature: true do + include AccessMatchers + describe "GET /dashboard" do subject { dashboard_path } diff --git a/spec/features/security/group/group_access_spec.rb b/spec/features/security/group/group_access_spec.rb deleted file mode 100644 index 63793149459..00000000000 --- a/spec/features/security/group/group_access_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'spec_helper' - -describe "Group access", feature: true do - describe "GET /projects/new" do - it { expect(new_group_path).to be_allowed_for :admin } - it { expect(new_group_path).to be_allowed_for :user } - it { expect(new_group_path).to be_denied_for :visitor } - end - - describe "Group" do - let(:group) { create(:group) } - - let(:owner) { create(:owner) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:nonmember) { create(:user) } - - before do - group.add_user(owner, Gitlab::Access::OWNER) - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - group.add_user(guest, Gitlab::Access::GUEST) - end - - describe "GET /groups/:path" do - subject { group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/issues" do - subject { issues_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/merge_requests" do - subject { merge_requests_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/group_members" do - subject { group_group_members_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/edit" do - subject { edit_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/projects" do - subject { projects_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - end -end diff --git a/spec/features/security/group/internal_group_access_spec.rb b/spec/features/security/group/internal_group_access_spec.rb deleted file mode 100644 index d17a7412e43..00000000000 --- a/spec/features/security/group/internal_group_access_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'spec_helper' - -describe "Group with internal project access", feature: true do - describe "Group" do - let(:group) { create(:group) } - - let(:owner) { create(:owner) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:nonmember) { create(:user) } - - before do - group.add_user(owner, Gitlab::Access::OWNER) - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - group.add_user(guest, Gitlab::Access::GUEST) - - create(:project, :internal, group: group) - end - - describe "GET /groups/:path" do - subject { group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/issues" do - subject { issues_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/merge_requests" do - subject { merge_requests_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/group_members" do - subject { group_group_members_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end - - describe "GET /groups/:path/edit" do - subject { edit_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - end -end diff --git a/spec/features/security/group/mixed_group_access_spec.rb b/spec/features/security/group/mixed_group_access_spec.rb deleted file mode 100644 index b3db7b5dea4..00000000000 --- a/spec/features/security/group/mixed_group_access_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' - -describe "Group access", feature: true do - describe "Group" do - let(:group) { create(:group) } - - let(:owner) { create(:owner) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:nonmember) { create(:user) } - - before do - group.add_user(owner, Gitlab::Access::OWNER) - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - group.add_user(guest, Gitlab::Access::GUEST) - - create(:project, :internal, path: "internal_project", group: group) - create(:project, :public, path: "public_project", group: group) - end - - describe "GET /groups/:path" do - subject { group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/issues" do - subject { issues_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/merge_requests" do - subject { merge_requests_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/group_members" do - subject { group_group_members_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/edit" do - subject { edit_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - end -end diff --git a/spec/features/security/group/public_group_access_spec.rb b/spec/features/security/group/public_group_access_spec.rb deleted file mode 100644 index c16f0c0d1e1..00000000000 --- a/spec/features/security/group/public_group_access_spec.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'spec_helper' - -describe "Group with public project access", feature: true do - describe "Group" do - let(:group) { create(:group) } - - let(:owner) { create(:owner) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - let(:nonmember) { create(:user) } - - before do - group.add_user(owner, Gitlab::Access::OWNER) - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - group.add_user(guest, Gitlab::Access::GUEST) - - create(:project, :public, group: group) - end - - describe "GET /groups/:path" do - subject { group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/issues" do - subject { issues_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/merge_requests" do - subject { merge_requests_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/group_members" do - subject { group_group_members_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :visitor } - end - - describe "GET /groups/:path/edit" do - subject { edit_group_path(group) } - - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_denied_for master } - it { is_expected.to be_denied_for reporter } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_denied_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :visitor } - end - end -end diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb new file mode 100644 index 00000000000..8ce15388605 --- /dev/null +++ b/spec/features/security/group_access_spec.rb @@ -0,0 +1,284 @@ +require 'rails_helper' + +describe 'Group access', feature: true do + include AccessMatchers + + def group + @group ||= create(:group) + end + + def create_project(access_level) + if access_level == :mixed + create(:empty_project, :public, group: group) + create(:empty_project, :internal, group: group) + else + create(:empty_project, access_level, group: group) + end + end + + def group_member(access_level, group = group) + level = Object.const_get("Gitlab::Access::#{access_level.upcase}") + + create(:user).tap do |user| + group.add_user(user, level) + end + end + + describe 'GET /groups/new' do + subject { new_group_path } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + + describe 'GET /groups/:path' do + subject { group_path(group) } + + context 'with public projects' do + let!(:project) { create_project(:public) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with mixed projects' do + let!(:project) { create_project(:mixed) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with internal projects' do + let!(:project) { create_project(:internal) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with no projects' do + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe 'GET /groups/:path/issues' do + subject { issues_group_path(group) } + + context 'with public projects' do + let!(:project) { create_project(:public) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with mixed projects' do + let!(:project) { create_project(:mixed) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with internal projects' do + let!(:project) { create_project(:internal) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with no projects' do + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe 'GET /groups/:path/merge_requests' do + subject { merge_requests_group_path(group) } + + context 'with public projects' do + let!(:project) { create_project(:public) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with mixed projects' do + let!(:project) { create_project(:mixed) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with internal projects' do + let!(:project) { create_project(:internal) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with no projects' do + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe 'GET /groups/:path/group_members' do + subject { group_group_members_path(group) } + + context 'with public projects' do + let!(:project) { create_project(:public) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with mixed projects' do + let!(:project) { create_project(:mixed) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context 'with internal projects' do + let!(:project) { create_project(:internal) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with no projects' do + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_allowed_for group_member(:master) } + it { is_expected.to be_allowed_for group_member(:reporter) } + it { is_expected.to be_allowed_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe 'GET /groups/:path/edit' do + subject { edit_group_path(group) } + + context 'with public projects' do + let!(:project) { create_project(:public) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_denied_for group_member(:master) } + it { is_expected.to be_denied_for group_member(:reporter) } + it { is_expected.to be_denied_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with mixed projects' do + let!(:project) { create_project(:mixed) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_denied_for group_member(:master) } + it { is_expected.to be_denied_for group_member(:reporter) } + it { is_expected.to be_denied_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with internal projects' do + let!(:project) { create_project(:internal) } + + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_denied_for group_member(:master) } + it { is_expected.to be_denied_for group_member(:reporter) } + it { is_expected.to be_denied_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + + context 'with no projects' do + it { is_expected.to be_allowed_for group_member(:owner) } + it { is_expected.to be_denied_for group_member(:master) } + it { is_expected.to be_denied_for group_member(:reporter) } + it { is_expected.to be_denied_for group_member(:guest) } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end +end diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index bcabc2d53ac..c19678ab381 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -1,18 +1,11 @@ require 'spec_helper' describe "Profile access", feature: true do - before do - @u1 = create(:user) - end - - describe "GET /login" do - it { expect(new_user_session_path).not_to be_not_found_for :visitor } - end + include AccessMatchers describe "GET /profile/keys" do subject { profile_keys_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } @@ -21,7 +14,6 @@ describe "Profile access", feature: true do describe "GET /profile" do subject { profile_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } @@ -30,7 +22,6 @@ describe "Profile access", feature: true do describe "GET /profile/account" do subject { profile_account_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } @@ -39,7 +30,6 @@ describe "Profile access", feature: true do describe "GET /profile/preferences" do subject { profile_preferences_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } @@ -48,7 +38,6 @@ describe "Profile access", feature: true do describe "GET /profile/audit_log" do subject { audit_log_profile_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } @@ -57,7 +46,6 @@ describe "Profile access", feature: true do describe "GET /profile/notifications" do subject { profile_notifications_path } - it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } it { is_expected.to be_denied_for :visitor } diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 4649e58cb1a..57563add74c 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Internal Project Access", feature: true do + include AccessMatchers + let(:project) { create(:project, :internal) } let(:master) { create(:user) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 2866bf0355b..a1e111c6cab 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Private Project Access", feature: true do + include AccessMatchers + let(:project) { create(:project) } let(:master) { create(:user) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 554c96bcdc5..655d2c8b7d9 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe "Public Project Access", feature: true do + include AccessMatchers + let(:project) { create(:project) } let(:master) { create(:user) } @@ -17,7 +19,6 @@ describe "Public Project Access", feature: true do # readonly project.team << [reporter, :reporter] - end describe "Project should be public" do diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index a4c3dfe9205..efcb8a31abe 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Users' do +feature 'Users', feature: true do scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path fill_in 'user_name', with: 'Name Surname' diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 2ab71b05968..de9d4cd6128 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -39,7 +39,7 @@ describe ProjectsFinder do end context 'authenticated, group member' do - before { group.add_user(user, Gitlab::Access::DEVELOPER) } + before { group.add_developer(user) } subject { ProjectsFinder.new.execute(user, group: group) } diff --git a/spec/fixtures/GoogleCodeProjectHosting.json b/spec/fixtures/GoogleCodeProjectHosting.json index d05e77271ae..67bb3bae5b7 100644 --- a/spec/fixtures/GoogleCodeProjectHosting.json +++ b/spec/fixtures/GoogleCodeProjectHosting.json @@ -382,6 +382,11 @@ "fileName" : "screenshot.png", "fileSize" : 0, "mimetype" : "image/png" + }, { + "attachmentId" : "001", + "fileName" : "screenshot1.PNG", + "fileSize" : 0, + "mimetype" : "image/x-png" } ] }, { "id" : 1, diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 02ab46c905a..41d12afa9ce 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try! - [**text**](#link-strong) - [`text`](#link-code) +### RelativeLinkFilter + +Linking to a file relative to this project's repository should work. + +[Relative Link](doc/README.md) + + ### EmojiFilter Because life would be :zzz: without Emoji, right? :rocket: @@ -123,9 +130,9 @@ These are all plain text that should get turned into links: But it shouldn't autolink text inside certain tags: -- <code>http://about.gitlab.com/</code> -- <a>http://about.gitlab.com/</a> -- <kbd>http://about.gitlab.com/</kbd> +- <code>http://code.gitlab.com/</code> +- <a>http://a.gitlab.com/</a> +- <kbd>http://kbd.gitlab.com/</kbd> ### ExternalLinkFilter diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb new file mode 100644 index 00000000000..e47a54fdac5 --- /dev/null +++ b/spec/helpers/auth_helper_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe AuthHelper do + describe "button_based_providers" do + it 'returns all enabled providers' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + expect(helper.button_based_providers).to include(*[:twitter, :github]) + end + + it 'does not return ldap provider' do + allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] } + expect(helper.button_based_providers).to include(:twitter) + end + + it 'returns empty array' do + allow(helper).to receive(:auth_providers) { [] } + expect(helper.button_based_providers).to eq([]) + end + end +end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index e49e4e6d5d8..b8bba36439a 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -6,6 +6,14 @@ describe BlobHelper do let(:no_context_content) { ":type \"assem\"))" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } let(:split_content) { blob_content.split("\n") } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end it 'should return plaintext for unknown lexer context' do result = highlight(blob_name, no_context_content, nowrap: true, continue: false) @@ -29,5 +37,31 @@ describe BlobHelper do result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } expect(result).to eq(expected) end + + it 'should highlight multi-line comments' do + result = highlight(blob_name, multiline_content, nowrap: true, continue: false) + html = Nokogiri::HTML(result) + lines = html.search('.s') + expect(lines.count).to eq(3) + expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') + end + + context 'diff highlighting' do + let(:blob_name) { 'test.diff' } + let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} + let(:expected) do + %q(<span id="LC1" class="line"><span class="gi">+aaa</span></span> +<span id="LC2" class="line"><span class="gi">+bbb</span></span> +<span id="LC3" class="line"><span class="gd">- ccc</span></span> +<span id="LC4" class="line"> ddd</span>) + end + + it 'should highlight each line properly' do + result = highlight(blob_name, blob_content, nowrap: true, continue: false) + expect(result).to eq(expected) + end + end end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index b392371deb4..da58ab98462 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -58,7 +58,7 @@ describe EventsHelper do expected = '<pre class="code highlight white ruby">' \ "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ " <span class=\"s1\">\'hello world\'</span>\n" \ - "<span class=\"k\">end</span>\n" \ + "<span class=\"k\">end</span>" \ '</code></pre>' expect(event_note(input)).to eq(expected) end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 14c8c29d008..a42ccb9b501 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -137,7 +137,7 @@ describe GitlabMarkdownHelper do describe 'random_markdown_tip' do it 'returns a random Markdown tip' do stub_const("#{described_class}::MARKDOWN_TIPS", ['Random tip']) - expect(random_markdown_tip).to eq 'Tip: Random tip' + expect(random_markdown_tip).to eq 'Random tip' end end end diff --git a/spec/helpers/oauth_helper_spec.rb b/spec/helpers/oauth_helper_spec.rb deleted file mode 100644 index 3ef35f35102..00000000000 --- a/spec/helpers/oauth_helper_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "spec_helper" - -describe OauthHelper do - describe "additional_providers" do - it 'returns all enabled providers' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github] } - expect(helper.additional_providers).to include(*[:twitter, :github]) - end - - it 'does not return ldap provider' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } - expect(helper.additional_providers).to include(:twitter) - end - - it 'returns empty array' do - allow(helper).to receive(:enabled_oauth_providers) { [] } - expect(helper.additional_providers).to eq([]) - end - end -end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 0f78725e3d9..99abb95d906 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -8,4 +8,66 @@ describe ProjectsHelper do expect(project_status_css_class("finished")).to eq("success") end end + + describe "can_change_visibility_level?" do + let(:project) { create(:project) } + + let(:fork_project) do + fork_project = create(:forked_project_with_submodules) + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + + fork_project + end + + let(:user) { create(:user) } + + it "returns false if there are no appropriate permissions" do + allow(helper).to receive(:can?) { false } + + expect(helper.can_change_visibility_level?(project, user)).to be_falsey + end + + it "returns true if there are permissions and it is not fork" do + allow(helper).to receive(:can?) { true } + + expect(helper.can_change_visibility_level?(project, user)).to be_truthy + end + + context "forks" do + it "returns false if there are permissions and origin project is PRIVATE" do + allow(helper).to receive(:can?) { true } + + project.update visibility_level: Gitlab::VisibilityLevel::PRIVATE + + expect(helper.can_change_visibility_level?(fork_project, user)).to be_falsey + end + + it "returns true if there are permissions and origin project is INTERNAL" do + allow(helper).to receive(:can?) { true } + + project.update visibility_level: Gitlab::VisibilityLevel::INTERNAL + + expect(helper.can_change_visibility_level?(fork_project, user)).to be_truthy + end + end + end + + describe "readme_cache_key" do + let(:project) { create(:project) } + + before do + helper.instance_variable_set(:@project, project) + end + + it "returns a valid cach key" do + expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme") + end + + it "returns a valid cache key if HEAD does not exist" do + allow(project).to receive(:commit) { nil } + + expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme") + end + end end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index 3840e64981f..c4f7693329c 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -72,4 +72,43 @@ describe VisibilityLevelHelper do end end end + + describe "skip_level?" do + describe "forks" do + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:fork_project) { create(:forked_project_with_submodules) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + end + + it "skips levels" do + expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy + expect(skip_level?(fork_project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey + expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey + end + end + + describe "non-forked project" do + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + it "skips levels" do + expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey + expect(skip_level?(project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey + expect(skip_level?(project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey + end + end + + describe "Snippet" do + let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + it "skips levels" do + expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey + expect(skip_level?(snippet, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey + expect(skip_level?(snippet, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey + end + end + + end end diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml index 15ad1d8968f..da1ebcdb23c 100644 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -2,7 +2,9 @@ .file-content .line-numbers - 1.upto(25) do |i| - %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + %i.fa.fa-link + = i %pre.code.highlight %code - 1.upto(25) do |i| diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee index 14fa487ff7f..57453c716a5 100644 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -48,6 +48,14 @@ describe 'LineHighlighter', -> clickLine(13) expect(spy).toHaveBeenPrevented() + it 'handles clicking on a child icon element', -> + spy = spyOn(@class, 'setHash').and.callThrough() + + $('#L13 i').mousedown().click() + + expect(spy).toHaveBeenCalledWith(13) + expect($('#LC13')).toHaveClass(@css) + describe 'without shiftKey', -> it 'highlights one line when clicked', -> clickLine(13) diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee index 6cc96fb68a0..a0cfba455ea 100644 --- a/spec/javascripts/merge_request_tabs_spec.js.coffee +++ b/spec/javascripts/merge_request_tabs_spec.js.coffee @@ -51,6 +51,12 @@ describe 'MergeRequestTabs', -> expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + it 'changes from diffs.html', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html') + + expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') + expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + it 'changes from notes', -> @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 4439775f612..9c115bbfc6a 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -29,6 +29,16 @@ describe ExtractsPath do assign_ref_vars expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") end + + context 'escaped sequences in ref' do + let(:ref) { "improve%2Fawesome" } + + it "id should have no escape sequences" do + assign_ref_vars + expect(@ref).to eq('improve/awesome') + expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") + end + end end describe '#extract_ref' do diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb new file mode 100644 index 00000000000..2e0a05088cc --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::InlineDiff do + describe '#processing' do + let(:diff) do + <<eos +--- a/test.rb ++++ b/test.rb +@@ -1,6 +1,6 @@ + class Test + def cleanup_string(input) + return nil if input.nil? +- input.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip ++ input.to_s.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip + end + end +eos + end + + let(:expected) do + ["--- a/test.rb\n", + "+++ b/test.rb\n", + "@@ -1,6 +1,6 @@\n", + " class Test\n", + " def cleanup_string(input)\n", + " return nil if input.nil?\n", + "- input.#!idiff-start!##!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", + "+ input.#!idiff-start!#to_s.#!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", + " end\n", + " end\n"] + end + + let(:subject) { Gitlab::InlineDiff.processing(diff.lines) } + + it 'should retain backslashes' do + expect(subject).to eq(expected) + end + end +end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index c53ddeb87b5..f49cbb7f532 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -65,6 +65,7 @@ describe Gitlab::GoogleCodeImport::Importer do expect(issue.description).to include('all the best!') expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)') expect(issue.description).to include('') + expect(issue.description).to include('') end it "imports issue comments" do 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 e8391cc7aca..58155284486 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -80,6 +80,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path option' do doc = filter("See #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index a10d43c9a02..05a02de4669 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -76,6 +76,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path context' do doc = filter("See #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index fa43d33794d..35b1ba5f132 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -73,6 +73,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("Issue #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path context' do doc = filter("Issue #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index e9f8ed277a5..fabe0411e46 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -30,6 +30,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path context' do doc = filter("Label #{reference}", only_path: true) link = doc.css('a').first.attr('href') 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 5945302a2da..5cef52b1916 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -61,6 +61,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("Merge #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path context' do doc = filter("Merge #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb index 5ee5310825d..7f4d67e403f 100644 --- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb @@ -1,3 +1,5 @@ +# encoding: UTF-8 + require 'spec_helper' module Gitlab::Markdown @@ -101,6 +103,20 @@ module Gitlab::Markdown expect(doc.at_css('a')['href']).to eq 'http://example.com' end + it 'supports Unicode filenames' do + path = 'files/images/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class).to receive(:file_exists?). + and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match '/raw/' + end + context 'when requested path is a file in the repo' do let(:requested_path) { 'doc/api/README.md' } include_examples :relative_to_requested diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 38619a3c07f..678b171e99e 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -60,6 +60,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to include 'custom' end + it 'includes a data-project-id attribute' do + doc = filter("Snippet #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project-id') + expect(link.attr('data-project-id')).to eq project.id.to_s + end + it 'supports an :only_path context' do doc = filter("Snippet #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index 08e6941028f..a5405e14a73 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -64,6 +64,14 @@ module Gitlab::Markdown expect(doc.css('a').length).to eq 1 end + it 'includes a data-user-id attribute' do + doc = filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user-id') + expect(link.attr('data-user-id')).to eq user.namespace.owner_id.to_s + end + it 'adds to the results hash' do result = pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq [user] @@ -77,7 +85,7 @@ module Gitlab::Markdown context 'that the current user can read' do before do - group.add_user(user, Gitlab::Access::DEVELOPER) + group.add_developer(user) end it 'links to the Group' do @@ -85,6 +93,14 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end + it 'includes a data-group-id attribute' do + doc = filter("Hey #{reference}", current_user: user) + link = doc.css('a').first + + expect(link).to have_attribute('data-group-id') + expect(link.attr('data-group-id')).to eq group.id.to_s + end + it 'adds to the results hash' do result = pipeline_result("Hey #{reference}", current_user: user) expect(result[:references][:user]).to eq group.users diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb index 7e716e866b1..e610fab05da 100644 --- a/spec/lib/gitlab/markup_helper_spec.rb +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::MarkupHelper do end describe '#gitlab_markdown?' do - %w(mdown md markdown).each do |type| + %w(mdown mkd mkdn md markdown).each do |type| it "returns true for #{type} files" do expect(Gitlab::MarkupHelper.gitlab_markdown?("README.#{type}")).to be_truthy end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index 5826144e66b..448cd0c6880 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -36,7 +36,6 @@ describe 'Gitlab::NoteDataBuilder' do let(:note) { create(:note_on_issue, noteable_id: issue.id) } it 'returns the note and issue-specific data' do - data[:issue]["updated_at"] = fixed_time expect(data).to have_key(:issue) expect(data[:issue]).to eq(issue.hook_attrs) end @@ -47,7 +46,6 @@ describe 'Gitlab::NoteDataBuilder' do let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } it 'returns the note and merge request data' do - data[:merge_request]["updated_at"] = fixed_time expect(data).to have_key(:merge_request) expect(data[:merge_request]).to eq(merge_request.hook_attrs) end @@ -58,7 +56,6 @@ describe 'Gitlab::NoteDataBuilder' do let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } it 'returns the note and merge request diff data' do - data[:merge_request]["updated_at"] = fixed_time expect(data).to have_key(:merge_request) expect(data[:merge_request]).to eq(merge_request.hook_attrs) end @@ -69,7 +66,6 @@ describe 'Gitlab::NoteDataBuilder' do let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } it 'returns the note and project snippet data' do - data[:snippet]["updated_at"] = fixed_time expect(data).to have_key(:snippet) expect(data[:snippet]).to eq(snippet.hook_attrs) end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index 4c0a4a49d2a..e4a6cd954cc 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -91,10 +91,6 @@ describe Gitlab::OAuth::AuthHash do expect(auth_hash.name.encoding).to eql Encoding::UTF_8 end - it 'forces utf8 encoding on full_name' do - expect(auth_hash.full_name.encoding).to eql Encoding::UTF_8 - end - it 'forces utf8 encoding on username' do expect(auth_hash.username.encoding).to eql Encoding::UTF_8 end diff --git a/spec/lib/gitlab/satellite/merge_action_spec.rb b/spec/lib/gitlab/satellite/merge_action_spec.rb index 9b1c9a34e29..e977261c726 100644 --- a/spec/lib/gitlab/satellite/merge_action_spec.rb +++ b/spec/lib/gitlab/satellite/merge_action_spec.rb @@ -101,4 +101,18 @@ describe 'Gitlab::Satellite::MergeAction' do end end end + + describe '#merge!' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "markdown", should_remove_source_branch: true) } + let(:merge_action) { Gitlab::Satellite::MergeAction.new(merge_request.author, merge_request) } + + it 'clears cache of source repo after removing source branch' do + project.repository.expire_branch_names + expect(project.repository.branch_names).to include('markdown') + + merge_action.merge! + + expect(project.repository.branch_names).not_to include('markdown') + end + end end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb new file mode 100644 index 00000000000..d83004a8388 --- /dev/null +++ b/spec/models/abuse_report_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe AbuseReport, type: :model do + subject { create(:abuse_report) } + + it { expect(subject).to be_valid } +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index d648f4078be..bc14ff98fd8 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -14,11 +14,14 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null -# session_expire_delay :integer default(10080), not null # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null # require 'spec_helper' diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index f7f66987b5f..2d6fe003215 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -28,4 +28,53 @@ describe Issue, "Mentionable" do issue.create_cross_references!(project, author, [commit2]) end end + + describe '#create_new_cross_references!' do + let(:project) { create(:project) } + let(:issues) { create_list(:issue, 2, project: project) } + + context 'before changes are persisted' do + it 'ignores pre-existing references' do + issue = create_issue(description: issues[0].to_reference) + + expect(SystemNoteService).not_to receive(:cross_reference) + + issue.description = 'New description' + issue.create_new_cross_references! + end + + it 'notifies new references' do + issue = create_issue(description: issues[0].to_reference) + + expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args) + + issue.description = issues[1].to_reference + issue.create_new_cross_references! + end + end + + context 'after changes are persisted' do + it 'ignores pre-existing references' do + issue = create_issue(description: issues[0].to_reference) + + expect(SystemNoteService).not_to receive(:cross_reference) + + issue.update_attributes(description: 'New description') + issue.create_new_cross_references! + end + + it 'notifies new references' do + issue = create_issue(description: issues[0].to_reference) + + expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args) + + issue.update_attributes(description: issues[1].to_reference) + issue.create_new_cross_references! + end + end + + def create_issue(description:) + create(:issue, project: project, description: description) + end + end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 4175f9dd88f..02d2cc2c77a 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -101,7 +101,7 @@ describe SystemHook do it 'group member create hook' do group = create(:group) user = create(:user) - group.add_user(user, Gitlab::Access::MASTER) + group.add_master(user) expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /user_add_to_group/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } @@ -111,7 +111,7 @@ describe SystemHook do it 'group member destroy hook' do group = create(:group) user = create(:user) - group.add_user(user, Gitlab::Access::MASTER) + group.add_master(user) group.group_members.destroy_all expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /user_remove_from_group/, diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index fbb9e162952..2f819f60cbb 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -32,6 +32,13 @@ describe Key do describe "Methods" do it { is_expected.to respond_to :projects } + it { is_expected.to respond_to :publishable_key } + + describe "#publishable_keys" do + it 'strips all personal information' do + expect(build(:key).publishable_key).not_to match(/dummy@gitlab/) + end + end end context "validation of uniqueness" do @@ -63,7 +70,7 @@ describe Key do key = build(:key) # Not always the middle, but close enough - key.key = key.key[0..100] + ' ' + key.key[100..-1] + key.key = key.key[0..100] + ' ' + key.key[101..-1] expect(key).not_to be_valid end @@ -71,6 +78,12 @@ describe Key do it 'rejects the unfingerprintable key (not a key)' do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + + it 'rejects the multiple line key' do + key = build(:key) + key.key.gsub!(' ', "\n") + expect(key).not_to be_valid + end end context 'callbacks' do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index eba33dd510f..250d1e2da80 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -22,7 +22,7 @@ require 'spec_helper' describe Note do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable).touch(true) } + it { is_expected.to belong_to(:noteable) } it { is_expected.to belong_to(:author).class_name('User') } end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 7e5b15cb09e..16296607a94 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -39,15 +39,19 @@ describe FlowdockService do token: 'verySecret' ) @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) - @api_url = 'https://api.flowdock.com/v1/git/verySecret' + @api_url = 'https://api.flowdock.com/v1/messages' WebMock.stub_request(:post, @api_url) end it "should call FlowDock API" do @flowdock_service.execute(@sample_data) - expect(WebMock).to have_requested(:post, @api_url).with( - body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ - ).once + @sample_data[:commits].each do |commit| + # One request to Flowdock per new commit + next if commit[:id] == @sample_data[:before] + expect(WebMock).to have_requested(:post, @api_url).with( + body: /#{commit[:id]}.*#{project.path}/ + ).once + end end end end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index fedc37c9b94..a14384c87b4 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -26,6 +26,33 @@ describe GitlabCiService do it { is_expected.to have_one(:service_hook) } end + describe 'validations' do + context 'active' do + before { allow(subject).to receive(:activated?).and_return(true) } + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } + it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) } + it { is_expected.not_to allow_value('token with spaces').for(:token) } + it { is_expected.not_to allow_value('token/with%spaces').for(:token) } + it { is_expected.not_to allow_value('this is not url').for(:project_url) } + it { is_expected.not_to allow_value('http//noturl').for(:project_url) } + it { is_expected.not_to allow_value('ftp://ci.example.com/projects/3').for(:project_url) } + end + + context 'inactive' do + before { allow(subject).to receive(:activated?).and_return(false) } + + it { is_expected.not_to validate_presence_of(:token) } + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } + it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) } + it { is_expected.to allow_value('token with spaces').for(:token) } + it { is_expected.to allow_value('ftp://ci.example.com/projects/3').for(:project_url) } + end + end + describe 'commits methods' do before do @service = GitlabCiService.new diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 4707673269a..65d16beef91 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -47,6 +47,14 @@ describe HipchatService do WebMock.stub_request(:post, api_url) end + it 'should test and return errors' do + allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room') + result = hipchat.test(push_sample_data) + + expect(result[:success]).to be_falsey + expect(result[:result].to_s).to eq('no such room') + end + it 'should use v1 if version is provided' do allow(hipchat).to receive(:api_version).and_return('v1') expect(HipChat::Client).to receive(:new). diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 63091e913ff..5d40754d59d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # require 'spec_helper' @@ -111,14 +112,20 @@ describe Project do expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') end - it 'returns the full web URL for this repo' do - project = Project.new(path: 'somewhere') - expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/somewhere") + describe "#web_url" do + let(:project) { create(:empty_project, path: "somewhere") } + + it 'returns the full web URL for this repo' do + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere") + end end - it 'returns the web URL without the protocol for this repo' do - project = Project.new(path: 'somewhere') - expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere") + describe "#web_url_without_protocol" do + let(:project) { create(:empty_project, path: "somewhere") } + + it 'returns the web URL without the protocol for this repo' do + expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere") + end end describe 'last_activity methods' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index d125166e336..cc1138490a0 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -37,9 +37,9 @@ describe ProjectTeam do let(:project) { create(:empty_project, group: group) } before do - group.add_user(master, Gitlab::Access::MASTER) - group.add_user(reporter, Gitlab::Access::REPORTER) - group.add_user(guest, Gitlab::Access::GUEST) + group.add_master(master) + group.add_reporter(reporter) + group.add_guest(guest) # If user is a group and a project member - GitLab uses highest permission # So we add group guest as master and add group master as guest diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a083dcb1274..8e2849691ff 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -34,17 +34,47 @@ describe Repository do end end - describe :can_be_merged? do - context 'mergeable branches' do - subject { repository.can_be_merged?('feature', 'master') } + describe :merged_to_root_ref? do + context 'merged branch' do + subject { repository.merged_to_root_ref?('improve/awesome') } it { is_expected.to be_truthy } end - context 'non-mergeable branches' do - subject { repository.can_be_merged?('feature_conflict', 'feature') } + context 'non merged branch' do + subject { repository.merged_to_root_ref?('fix') } it { is_expected.to be_falsey } end + + context 'non existent branch' do + subject { repository.merged_to_root_ref?('non_existent_branch') } + + it { is_expected.to be_nil } + end + end + + describe "search_files" do + let(:results) { repository.search_files('feature', 'master') } + subject { results } + + it { is_expected.to be_an Array } + + describe 'result' do + subject { results.first } + + it { is_expected.to be_an String } + it { expect(subject.lines[2]).to eq("master:CHANGELOG:188: - Feature: Replace teams with group membership\n") } + end + + describe 'parsing result' do + subject { repository.parse_search_result(results.first) } + + it { is_expected.to be_an OpenStruct } + it { expect(subject.filename).to eq('CHANGELOG') } + it { expect(subject.ref).to eq('master') } + it { expect(subject.startline).to eq(186) } + it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } + end end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index ca11758ee06..a213ffe6c4b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -46,6 +46,16 @@ describe Service do describe :can_test do it { expect(@testable).to eq(true) } end + + describe :test do + let(:data) { 'test' } + + it 'test runs execute' do + expect(@service).to receive(:execute).with(data) + + @service.test(data) + end + end end describe "With commits" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d2423ae27a..876cfb1204a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -57,6 +57,7 @@ # otp_backup_codes :text # public_email :string(255) default(""), not null # dashboard :integer default(0) +# project_view :integer default(0) # require 'spec_helper' @@ -183,6 +184,19 @@ describe User do it { is_expected.to respond_to(:private_token) } end + describe '#confirm' do + let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } + + it 'returns unconfirmed' do + expect(user.confirmed?).to be_falsey + end + + it 'confirms a user' do + user.confirm! + expect(user.confirmed?).to be_truthy + end + end + describe '#to_reference' do let(:user) { create(:user) } @@ -217,6 +231,24 @@ describe User do end end + describe '#disable_two_factor!' do + it 'clears all 2FA-related fields' do + user = create(:user, :two_factor) + + expect(user).to be_two_factor_enabled + expect(user.encrypted_otp_secret).not_to be_nil + expect(user.otp_backup_codes).not_to be_nil + + user.disable_two_factor! + + expect(user).not_to be_two_factor_enabled + expect(user.encrypted_otp_secret).to be_nil + expect(user.encrypted_otp_secret_iv).to be_nil + expect(user.encrypted_otp_secret_salt).to be_nil + expect(user.otp_backup_codes).to be_nil + end + end + describe 'projects' do before do @user = create :user @@ -424,6 +456,18 @@ describe User do end end + describe '.find_by_username!' do + it 'raises RecordNotFound' do + expect { described_class.find_by_username!('JohnDoe') }. + to raise_error(ActiveRecord::RecordNotFound) + end + + it 'is case-insensitive' do + user = create(:user, username: 'JohnDoe') + expect(described_class.find_by_username!('JOHNDOE')).to eq user + end + end + describe 'all_ssh_keys' do it { is_expected.to have_many(:keys).dependent(:destroy) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index cb6e5e89625..5c1b58535cc 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -14,10 +14,13 @@ describe API::API, api: true do describe "GET /projects/:id/repository/branches" do it "should return an array of project branches" do + project.repository.expire_cache + get api("/projects/#{project.id}/repository/branches", user) expect(response.status).to eq(200) expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.repository.branch_names.first) + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 6c7860511e8..78f2cb56b02 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -49,6 +49,8 @@ describe API::API, api: true do end it "should create a new file in project repo" do + expect_any_instance_of(Gitlab::Satellite::NewFileAction).to receive(:commit!).and_return(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') @@ -59,9 +61,8 @@ describe API::API, api: true do expect(response.status).to eq(400) end - it "should return a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). - and_return(false) + it "should return a 400 if satellite fails to create file" do + expect_any_instance_of(Gitlab::Satellite::NewFileAction).to receive(:commit!).and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(400) @@ -79,6 +80,8 @@ describe API::API, api: true do end it "should update existing file in project repo" do + expect_any_instance_of(Gitlab::Satellite::EditFileAction).to receive(:commit!).and_return(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) @@ -88,6 +91,32 @@ 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 + expect_any_instance_of(Gitlab::Satellite::EditFileAction).to receive(: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 + expect_any_instance_of(Gitlab::Satellite::EditFileAction).to receive(: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 + expect_any_instance_of(Gitlab::Satellite::EditFileAction).to receive(: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 @@ -100,6 +129,7 @@ describe API::API, api: true do end it "should delete existing file in project repo" do + expect_any_instance_of(Gitlab::Satellite::DeleteFileAction).to receive(:commit!).and_return(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) @@ -111,10 +141,41 @@ describe API::API, api: true do end it "should return a 400 if satellite fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + expect_any_instance_of(Gitlab::Satellite::DeleteFileAction).to receive(:commit!).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(400) end end + + describe "POST /projects/:id/repository/files with binary file" do + let(:file_path) { 'test.bin' } + let(:put_params) do + { + file_path: file_path, + branch_name: 'master', + content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', + commit_message: 'Binary file with a \n should not be touched', + encoding: 'base64' + } + end + let(:get_params) do + { + file_path: file_path, + ref: 'master', + } + end + + before do + post api("/projects/#{project.id}/repository/files", user), put_params + end + + it "remains unchanged" do + get api("/projects/#{project.id}/repository/files", user), get_params + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq(file_path) + expect(json_response['content']).to eq(put_params[:content]) + end + end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index c5a4ac7e4c4..1d5b4f6f36b 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -135,7 +135,7 @@ describe API::API, api: true do it "should not remove a group if not an owner" do user4 = create(:user) - group1.add_user(user4, Gitlab::Access::MASTER) + group1.add_master(user4) delete api("/groups/#{group1.id}", user3) expect(response.status).to eq(403) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7030c105b58..29db035b2de 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -8,6 +8,7 @@ describe API::API, api: true do let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test") } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test") } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } + let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } before do project.team << [user, :reporters] @@ -397,13 +398,14 @@ describe API::API, api: true do end describe "GET :id/merge_request/:merge_request_id/comments" do - it "should return merge_request comments" do + it "should return merge_request comments ordered by created_at" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq("a comment on a MR") expect(json_response.first['author']['id']).to eq(user.id) + expect(json_response.last['note']).to eq("another comment on a MR") end it "should return a 404 error if merge_request_id not found" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e9ff832603f..5bd8206b890 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -89,7 +89,7 @@ describe API::API, api: true do 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") + project2.gitlab_ci_service.update(active: true, token: "token", project_url: "http://ci.example.com/projects/1") get api('/projects', user), { ci_enabled_first: 'true' } expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -220,9 +220,7 @@ describe API::API, api: true do context 'when a visibility level is restricted' do before do @project = attributes_for(:project, { public: true }) - allow_any_instance_of(ApplicationSetting).to( - receive(:restricted_visibility_levels).and_return([20]) - ) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end it 'should not allow a non-admin to use a restricted visibility level' do diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 51c543578df..6d29a28580a 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -7,7 +7,7 @@ describe API::API, api: true do describe "POST /projects/:id/services/gitlab-ci" do it "should update gitlab-ci settings" do - put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secret-token', project_url: "http://ci.example.com/projects/1" + put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secrettoken', project_url: "http://ci.example.com/projects/1" expect(response.status).to eq(200) end @@ -17,6 +17,18 @@ describe API::API, api: true do expect(response.status).to eq(400) end + + it "should return if the format of token is invalid" do + put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "http://ci.example.com/projects/1", active: true + + expect(response.status).to eq(404) + end + + it "should return if the format of token is invalid" do + put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "ftp://ci.example/projects/1", active: true + + expect(response.status).to eq(404) + end end describe "DELETE /projects/:id/services/gitlab-ci" do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c4dd1f76cf2..f2aa369985e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -6,6 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } describe "GET /users" do context "when unauthenticated" do @@ -384,6 +385,87 @@ describe API::API, api: true do end end + describe "POST /users/:id/emails" do + before { admin } + + it "should not create invalid email" do + post api("/users/#{user.id}/emails", admin), {} + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "email" not given') + end + + it "should create email" do + email_attrs = attributes_for :email + expect do + post api("/users/#{user.id}/emails", admin), email_attrs + end.to change{ user.emails.count }.by(1) + end + end + + describe 'GET /user/:uid/emails' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + get api("/users/#{user.id}/emails") + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'should return 404 for non-existing user' do + get api('/users/999999/emails', admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'should return array of emails' do + user.emails << email + user.save + get api("/users/#{user.id}/emails", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['email']).to eq(email.email) + end + end + end + + describe 'DELETE /user/:uid/emails/:id' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + delete api("/users/#{user.id}/emails/42") + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'should delete existing email' do + user.emails << email + user.save + expect do + delete api("/users/#{user.id}/emails/#{email.id}", admin) + end.to change { user.emails.count }.by(-1) + expect(response.status).to eq(200) + end + + it 'should return 404 error if user not found' do + user.emails << email + user.save + delete api("/users/999999/emails/#{email.id}", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'should return 404 error if email not foud' do + delete api("/users/#{user.id}/emails/42", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Email Not Found') + end + end + end + describe "DELETE /users/:id" do before { admin } @@ -528,6 +610,95 @@ describe API::API, api: true do end end + describe "GET /user/emails" do + context "when unauthenticated" do + it "should return authentication error" do + get api("/user/emails") + expect(response.status).to eq(401) + end + end + + context "when authenticated" do + it "should return array of emails" do + user.emails << email + user.save + get api("/user/emails", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first["email"]).to eq(email.email) + end + end + end + + describe "GET /user/emails/:id" do + it "should return single email" do + user.emails << email + user.save + get api("/user/emails/#{email.id}", user) + expect(response.status).to eq(200) + expect(json_response["email"]).to eq(email.email) + end + + it "should return 404 Not Found within invalid ID" do + get api("/user/emails/42", user) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') + end + + it "should return 404 error if admin accesses user's email" do + user.emails << email + user.save + admin + get api("/user/emails/#{email.id}", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + describe "POST /user/emails" do + it "should create email" do + email_attrs = attributes_for :email + expect do + post api("/user/emails", user), email_attrs + end.to change{ user.emails.count }.by(1) + expect(response.status).to eq(201) + end + + it "should return a 401 error if unauthorized" do + post api("/user/emails"), email: 'some email' + expect(response.status).to eq(401) + end + + it "should not create email with invalid email" do + post api("/user/emails", user), {} + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "email" not given') + end + end + + describe "DELETE /user/emails/:id" do + it "should delete existed email" do + user.emails << email + user.save + expect do + delete api("/user/emails/#{email.id}", user) + end.to change{user.emails.count}.by(-1) + expect(response.status).to eq(200) + end + + it "should return success if email ID not found" do + delete api("/user/emails/42", user) + expect(response.status).to eq(200) + end + + it "should return 401 error if unauthorized" do + user.emails << email + user.save + delete api("/user/emails/#{email.id}") + expect(response.status).to eq(401) + end + end + describe 'PUT /user/:id/block' do before { admin } it 'should block existing user' do diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index 08689c15ca8..8edabe9450b 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -14,11 +14,7 @@ describe CreateSnippetService do context 'When public visibility is restricted' do before do - allow_any_instance_of(ApplicationSetting).to( - receive(:restricted_visibility_levels).and_return( - [Gitlab::VisibilityLevel::PUBLIC] - ) - ) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 3373b97bfd4..62cef9db534 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -124,9 +124,7 @@ describe GitPushService do end it "when pushing a branch for the first time with default branch protection disabled" do - allow(ApplicationSetting.current_application_settings). - to receive(:default_branch_protection). - and_return(Gitlab::Access::PROTECTION_NONE) + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") @@ -135,9 +133,7 @@ describe GitPushService do end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do - allow(ApplicationSetting.current_application_settings). - to receive(:default_branch_protection). - and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 253e5823499..9da6c9dc949 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -300,7 +300,7 @@ describe NotificationService do describe 'Merge Requests' do let(:project) { create(:project, :public) } - let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user) } + let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' } before do build_team(merge_request.target_project) @@ -311,6 +311,7 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.new_merge_request(merge_request, @u_disabled) @@ -329,6 +330,7 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_email(@subscriber.id) should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) @@ -349,6 +351,7 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_email(@subscriber.id) should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) @@ -369,6 +372,7 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_email(@subscriber.id) should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) @@ -389,6 +393,7 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_email(@subscriber.id) should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 337dae592dd..66cdfd5d758 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -4,13 +4,19 @@ describe Projects::CreateService do describe :create_by_user do before do @user = create :user - @admin = create :user, admin: true @opts = { name: "GitLab", namespace: @user.namespace } end + it 'creates services on Project creation' do + project = create_project(@user, @opts) + project.reload + + expect(project.services).not_to be_empty + end + context 'user namespace' do before do @project = create_project(@user, @opts) @@ -58,9 +64,7 @@ describe Projects::CreateService do context 'restricted visibility level' do before do - allow_any_instance_of(ApplicationSetting).to( - receive(:restricted_visibility_levels).and_return([20]) - ) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @opts.merge!( visibility_level: Gitlab::VisibilityLevel.options['Public'] @@ -77,7 +81,9 @@ describe Projects::CreateService do end it 'should allow a restricted visibility level for admins' do - project = create_project(@admin, @opts) + admin = create(:admin) + project = create_project(admin, @opts) + expect(project.errors.any?).to be(false) expect(project.saved?).to be(true) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 439a492cea9..c04e842c67e 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -29,7 +29,7 @@ describe Projects::ForkService do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) expect(@to_project.errors).not_to be_empty - expect(@to_project.errors[:base]).to include("Failed to fork repository") + expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell") end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 79acba78bda..bb7da33b12e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -8,7 +8,7 @@ describe Projects::TransferService do context 'namespace -> namespace' do before do group.add_owner(user) - @result = transfer_project(project, user, new_namespace_id: group.id) + @result = transfer_project(project, user, group) end it { expect(@result).to be_truthy } @@ -17,7 +17,7 @@ describe Projects::TransferService do context 'namespace -> no namespace' do before do - @result = transfer_project(project, user, new_namespace_id: nil) + @result = transfer_project(project, user, nil) end it { expect(@result).to eq false } @@ -26,14 +26,14 @@ describe Projects::TransferService do context 'namespace -> not allowed namespace' do before do - @result = transfer_project(project, user, new_namespace_id: group.id) + @result = transfer_project(project, user, group) end it { expect(@result).to eq false } it { expect(project.namespace).to eq(user.namespace) } end - def transfer_project(project, user, params) - Projects::TransferService.new(project, user, params).execute + def transfer_project(project, user, new_namespace) + Projects::TransferService.new(project, user).execute(new_namespace) end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 0dd6980a44f..b347fa15f87 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -47,9 +47,7 @@ describe Projects::UpdateService do context 'respect configured visibility restrictions setting' do before(:each) do - allow_any_instance_of(ApplicationSetting).to( - receive(:restricted_visibility_levels).and_return([20]) - ) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end context 'should be private when updated to private' do diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 841ef9bfed1..d7c516e3934 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -14,11 +14,7 @@ describe UpdateSnippetService do context 'When public visibility is restricted' do before do - allow_any_instance_of(ApplicationSetting).to( - receive(:restricted_visibility_levels).and_return( - [Gitlab::VisibilityLevel::PUBLIC] - ) - ) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @snippet = create_snippet(@project, @user, @opts) @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 682a8863bad..d0f1873ee2d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,15 @@ +if ENV['SIMPLECOV'] + require 'simplecov' + SimpleCov.start :rails +end + +if ENV['COVERALLS'] + require 'coveralls' + Coveralls.wear_merged! +end + ENV["RAILS_ENV"] ||= 'test' + require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb deleted file mode 100644 index a54bf03380c..00000000000 --- a/spec/support/coverage.rb +++ /dev/null @@ -1,8 +0,0 @@ -if ENV['SIMPLECOV'] - require 'simplecov' -end - -if ENV['COVERALLS'] - require 'coveralls' - Coveralls.wear_merged! -end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb new file mode 100644 index 00000000000..c59df4e84d6 --- /dev/null +++ b/spec/support/markdown_feature.rb @@ -0,0 +1,106 @@ +# This is a helper class used by the GitLab Markdown feature spec +# +# Because the feature spec only cares about the output of the Markdown, and the +# test setup and teardown and parsing is fairly expensive, we only want to do it +# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` +# block, so we fake it by encapsulating all the shared setup in this class. +# +# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for +# reference to the factory-created objects. +class MarkdownFeature + include FactoryGirl::Syntax::Methods + + def user + @user ||= create(:user) + end + + def group + unless @group + @group = create(:group) + @group.add_developer(user) + end + + @group + end + + # Direct references ---------------------------------------------------------- + + def project + @project ||= create(:project) + end + + def issue + @issue ||= create(:issue, project: project) + end + + def merge_request + @merge_request ||= create(:merge_request, :simple, source_project: project) + end + + def snippet + @snippet ||= create(:project_snippet, project: project) + end + + def commit + @commit ||= project.commit + end + + def commit_range + unless @commit_range + commit2 = project.commit('HEAD~3') + @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + end + + @commit_range + end + + def simple_label + @simple_label ||= create(:label, name: 'gfm', project: project) + end + + def label + @label ||= create(:label, name: 'awaiting feedback', project: project) + end + + # Cross-references ----------------------------------------------------------- + + def xproject + unless @xproject + namespace = create(:namespace, name: 'cross-reference') + @xproject = create(:project, namespace: namespace) + @xproject.team << [user, :developer] + end + + @xproject + end + + def xissue + @xissue ||= create(:issue, project: xproject) + end + + def xmerge_request + @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) + end + + def xsnippet + @xsnippet ||= create(:project_snippet, project: xproject) + end + + def xcommit + @xcommit ||= xproject.commit + end + + def xcommit_range + unless @xcommit_range + xcommit2 = xproject.commit('HEAD~2') + @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + end + + @xcommit_range + end + + def raw_markdown + fixture = Rails.root.join('spec/fixtures/markdown.md.erb') + ERB.new(File.read(fixture)).result(binding) + end +end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb deleted file mode 100644 index a2f853e3e70..00000000000 --- a/spec/support/matchers.rb +++ /dev/null @@ -1,66 +0,0 @@ -RSpec::Matchers.define :be_valid_commit do - match do |actual| - actual && - actual.id == ValidCommit::ID && - actual.message == ValidCommit::MESSAGE && - actual.author_name == ValidCommit::AUTHOR_FULL_NAME - end -end - -def emulate_user(user) - user = case user - when :user then create(:user) - when :visitor then nil - when :admin then create(:admin) - else user - end - login_with(user) if user -end - -RSpec::Matchers.define :be_allowed_for do |user| - match do |url| - emulate_user(user) - visit url - status_code != 404 && current_path != new_user_session_path - end -end - -RSpec::Matchers.define :be_denied_for do |user| - match do |url| - emulate_user(user) - visit url - status_code == 404 || current_path == new_user_session_path - end -end - -RSpec::Matchers.define :be_not_found_for do |user| - match do |url| - emulate_user(user) - visit url - status_code == 404 - end -end - -RSpec::Matchers.define :include_module do |expected| - match do - described_class.included_modules.include?(expected) - end - - description do - "includes the #{expected} module" - end - - failure_message do - "expected #{described_class} to include the #{expected} module" - end -end - -# Extend shoulda-matchers -module Shoulda::Matchers::ActiveModel - class ValidateLengthOfMatcher - # Shortcut for is_at_least and is_at_most - def is_within(range) - is_at_least(range.min) && is_at_most(range.max) - end - end -end diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb new file mode 100644 index 00000000000..558e8b1612f --- /dev/null +++ b/spec/support/matchers/access_matchers.rb @@ -0,0 +1,54 @@ +# AccessMatchers +# +# The custom matchers contained in this module are used to test a user's access +# to a URL by emulating a specific user or type of user account, visiting the +# URL, and then checking the response status code and resulting path. +module AccessMatchers + extend RSpec::Matchers::DSL + include Warden::Test::Helpers + + def emulate_user(user) + case user + when :user + login_as(create(:user)) + when :visitor + logout + when :admin + login_as(create(:admin)) + when User + login_as(user) + else + raise ArgumentError, "cannot emulate user #{user}" + end + end + + def description_for(user, type) + if user.kind_of?(User) + # User#inspect displays too much information for RSpec's description + # messages + "be #{type} for supplied User" + else + "be #{type} for #{user}" + end + end + + matcher :be_allowed_for do |user| + match do |url| + emulate_user(user) + visit url + status_code != 404 && current_path != new_user_session_path + end + + description { description_for(user, 'allowed') } + end + + matcher :be_denied_for do |user| + match do |url| + emulate_user(user) + visit url + status_code == 404 || current_path == new_user_session_path + end + + description { description_for(user, 'denied') } + end +end diff --git a/spec/support/matchers/include_module.rb b/spec/support/matchers/include_module.rb new file mode 100644 index 00000000000..0a78af1e90e --- /dev/null +++ b/spec/support/matchers/include_module.rb @@ -0,0 +1,13 @@ +RSpec::Matchers.define :include_module do |expected| + match do + described_class.included_modules.include?(expected) + end + + description do + "includes the #{expected} module" + end + + failure_message do + "expected #{described_class} to include the #{expected} module" + end +end diff --git a/spec/support/matchers/is_within.rb b/spec/support/matchers/is_within.rb new file mode 100644 index 00000000000..0c35fc7e899 --- /dev/null +++ b/spec/support/matchers/is_within.rb @@ -0,0 +1,9 @@ +# Extend shoulda-matchers +module Shoulda::Matchers::ActiveModel + class ValidateLengthOfMatcher + # Shortcut for is_at_least and is_at_most + def is_within(range) + is_at_least(range.min) && is_at_most(range.max) + end + end +end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb new file mode 100644 index 00000000000..9df226c3af8 --- /dev/null +++ b/spec/support/matchers/markdown_matchers.rb @@ -0,0 +1,156 @@ +# MarkdownMatchers +# +# Custom matchers for our custom HTML::Pipeline filters. These are used to test +# that specific filters are or are not used by our defined pipelines. +# +# Must be included manually. +module MarkdownMatchers + extend RSpec::Matchers::DSL + include Capybara::Node::Matchers + + # RelativeLinkFilter + matcher :parse_relative_links do + set_default_markdown_messages + + match do |actual| + link = actual.at_css('a:contains("Relative Link")') + image = actual.at_css('img[alt="Relative Image"]') + + expect(link['href']).to end_with('master/doc/README.md') + expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png') + end + end + + # EmojiFilter + matcher :parse_emoji do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('img.emoji', count: 10) + end + end + + # TableOfContentsFilter + matcher :create_header_links do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('h1 a#gitlab-markdown') + expect(actual).to have_selector('h2 a#markdown') + expect(actual).to have_selector('h3 a#autolinkfilter') + end + end + + # AutolinkFilter + matcher :create_autolinks do + def have_autolink(link) + have_link(link, href: link) + end + + set_default_markdown_messages + + match do |actual| + expect(actual).to have_autolink('http://about.gitlab.com/') + expect(actual).to have_autolink('https://google.com/') + expect(actual).to have_autolink('ftp://ftp.us.debian.org/debian/') + expect(actual).to have_autolink('smb://foo/bar/baz') + expect(actual).to have_autolink('irc://irc.freenode.net/git') + expect(actual).to have_autolink('http://localhost:3000') + + %w(code a kbd).each do |elem| + expect(body).not_to have_selector("#{elem} a") + end + end + end + + # UserReferenceFilter + matcher :reference_users do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3) + end + end + + # IssueReferenceFilter + matcher :reference_issues do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-issue', count: 3) + end + end + + # MergeRequestReferenceFilter + matcher :reference_merge_requests do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3) + expect(actual).to have_selector('em a.gfm-merge_request') + end + end + + # SnippetReferenceFilter + matcher :reference_snippets do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2) + end + end + + # CommitRangeReferenceFilter + matcher :reference_commit_ranges do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2) + end + end + + # CommitReferenceFilter + matcher :reference_commits do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-commit', count: 2) + end + end + + # LabelReferenceFilter + matcher :reference_labels do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-label', count: 3) + end + end + + # TaskListFilter + matcher :parse_task_lists do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('ul.task-list', count: 2) + expect(actual).to have_selector('li.task-list-item', count: 7) + expect(actual).to have_selector('input[checked]', count: 3) + end + end +end + +# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for +# setting the failure messages for these matchers +module RSpec::Matchers::DSL::Macros + def set_default_markdown_messages + failure_message do + # expected to parse emoji, but didn't + "expected to #{description}, but didn't" + end + + failure_message_when_negated do + # expected not to parse task lists, but did + "expected not to #{description}, but did" + end + end +end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index a2a0b6905f9..f0717e61781 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -143,6 +143,6 @@ shared_examples 'an editable mentionable' do end set_mentionable_text.call(new_text) - subject.notice_added_references(project, author) + subject.create_new_cross_references!(project, author) end end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index ad86abdbb41..e4004ec8f79 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -1,5 +1,10 @@ module StubConfiguration def stub_application_setting(messages) + add_predicates(messages) + + # Stubbing both of these because we're not yet consistent with how we access + # current application settings + allow_any_instance_of(ApplicationSetting).to receive_messages(messages) allow(Gitlab::CurrentSettings.current_application_settings). to receive_messages(messages) end @@ -11,4 +16,25 @@ module StubConfiguration def stub_gravatar_setting(messages) allow(Gitlab.config.gravatar).to receive_messages(messages) end + + private + + # Modifies stubbed messages to also stub possible predicate versions + # + # Examples: + # + # add_predicates(foo: true) + # # => {foo: true, foo?: true} + # + # add_predicates(signup_enabled?: false) + # # => {signup_enabled? false} + def add_predicates(messages) + # Only modify keys that aren't already predicates + keys = messages.keys.map(&:to_s).reject { |k| k.end_with?('?') } + + keys.each do |key| + predicate = key + '?' + messages[predicate.to_sym] = messages[key.to_sym] + end + end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 8bdd6b43cdd..8dc687c3580 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,18 +5,24 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { + 'empty-branch' => '7efb185', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', 'fix' => '12d65c8', 'improve/awesome' => '5937ac0', 'markdown' => '0ed8c6c', - 'master' => '5937ac0' + 'master' => '5937ac0', + "'test'" => 'e56497b', } - FORKED_BRANCH_SHA = BRANCH_SHA.merge({ - 'add-submodule-version-bump' => '3f547c08' - }) + # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily + # need to keep all the branches in sync. + # We currently only need a subset of the branches + FORKED_BRANCH_SHA = { + 'add-submodule-version-bump' => '3f547c08', + 'master' => '5937ac0' + } # Test environment # @@ -29,6 +35,7 @@ module TestEnv clean_test_path FileUtils.mkdir_p(repos_path) + FileUtils.mkdir_p(backup_path) # Setup GitLab shell for test instance setup_gitlab_shell @@ -121,6 +128,10 @@ module TestEnv Gitlab.config.gitlab_shell.repos_path end + def backup_path + Gitlab.config.backup.path + end + def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git") diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index cdcfeba8d1f..23f322e0a62 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -15,6 +15,12 @@ describe 'gitlab:app namespace rake task' do Rake.application.invoke_task task_name end + def reenable_backup_sub_tasks + %w{db repo uploads}.each do |subtask| + Rake::Task["gitlab:backup:#{subtask}:create"].reenable + end + end + describe 'backup_restore' do before do # avoid writing task output to spec progress @@ -60,26 +66,47 @@ describe 'gitlab:app namespace rake task' do Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) end - before :all do - # Record the existing backup tars so we don't touch them - existing_tars = tars_glob + def create_backup + FileUtils.rm tars_glob # Redirect STDOUT and run the rake task orig_stdout = $stdout $stdout = StringIO.new + reenable_backup_sub_tasks run_rake_task('gitlab:backup:create') + reenable_backup_sub_tasks $stdout = orig_stdout - @backup_tar = (tars_glob - existing_tars).first + @backup_tar = tars_glob.first end - after :all do + before do + create_backup + end + + after do FileUtils.rm(@backup_tar) end - it 'should set correct permissions on the tar file' do - expect(File.exist?(@backup_tar)).to be_truthy - expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + context 'archive file permissions' do + it 'should set correct permissions on the tar file' do + expect(File.exist?(@backup_tar)).to be_truthy + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + end + + context 'with custom archive_permissions' do + before do + allow(Gitlab.config.backup).to receive(:archive_permissions).and_return(0651) + # We created a backup in a before(:all) so it got the default permissions. + # We now need to do some work to create a _new_ backup file using our stub. + FileUtils.rm(@backup_tar) + create_backup + end + + it 'uses the custom permissions' do + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100651') + end + end end it 'should set correct permissions on the tar contents' do @@ -110,12 +137,9 @@ describe 'gitlab:app namespace rake task' do before :all do @origin_cd = Dir.pwd - Rake::Task["gitlab:backup:db:create"].reenable - Rake::Task["gitlab:backup:repo:create"].reenable - Rake::Task["gitlab:backup:uploads:create"].reenable + reenable_backup_sub_tasks - # Record the existing backup tars so we don't touch them - existing_tars = tars_glob + FileUtils.rm tars_glob # Redirect STDOUT and run the rake task orig_stdout = $stdout @@ -124,7 +148,7 @@ describe 'gitlab:app namespace rake task' do run_rake_task('gitlab:backup:create') $stdout = orig_stdout - @backup_tar = (tars_glob - existing_tars).first + @backup_tar = tars_glob.first end after :all do diff --git a/vendor/assets/javascripts/jquery.nicescroll.min.js b/vendor/assets/javascripts/jquery.nicescroll.min.js new file mode 100644 index 00000000000..5440b6a0da0 --- /dev/null +++ b/vendor/assets/javascripts/jquery.nicescroll.min.js @@ -0,0 +1,118 @@ +/* jquery.nicescroll 3.6.0 InuYaksa*2014 MIT http://nicescroll.areaaperta.com */(function(f){"function"===typeof define&&define.amd?define(["jquery"],f):f(jQuery)})(function(f){var y=!1,D=!1,N=0,O=2E3,x=0,H=["webkit","ms","moz","o"],s=window.requestAnimationFrame||!1,t=window.cancelAnimationFrame||!1;if(!s)for(var P in H){var E=H[P];s||(s=window[E+"RequestAnimationFrame"]);t||(t=window[E+"CancelAnimationFrame"]||window[E+"CancelRequestAnimationFrame"])}var v=window.MutationObserver||window.WebKitMutationObserver||!1,I={zindex:"auto",cursoropacitymin:0,cursoropacitymax:1,cursorcolor:"#424242", +cursorwidth:"5px",cursorborder:"1px solid #fff",cursorborderradius:"5px",scrollspeed:60,mousescrollstep:24,touchbehavior:!1,hwacceleration:!0,usetransition:!0,boxzoom:!1,dblclickzoom:!0,gesturezoom:!0,grabcursorenabled:!0,autohidemode:!0,background:"",iframeautoresize:!0,cursorminheight:32,preservenativescrolling:!0,railoffset:!1,railhoffset:!1,bouncescroll:!0,spacebarenabled:!0,railpadding:{top:0,right:0,left:0,bottom:0},disableoutline:!0,horizrailenabled:!0,railalign:"right",railvalign:"bottom", +enabletranslate3d:!0,enablemousewheel:!0,enablekeyboard:!0,smoothscroll:!0,sensitiverail:!0,enablemouselockapi:!0,cursorfixedheight:!1,directionlockdeadzone:6,hidecursordelay:400,nativeparentscrolling:!0,enablescrollonselection:!0,overflowx:!0,overflowy:!0,cursordragspeed:.3,rtlmode:"auto",cursordragontouch:!1,oneaxismousemode:"auto",scriptpath:function(){var f=document.getElementsByTagName("script"),f=f[f.length-1].src.split("?")[0];return 0<f.split("/").length?f.split("/").slice(0,-1).join("/")+ +"/":""}(),preventmultitouchscrolling:!0},F=!1,Q=function(){if(F)return F;var f=document.createElement("DIV"),c=f.style,h=navigator.userAgent,m=navigator.platform,d={haspointerlock:"pointerLockElement"in document||"webkitPointerLockElement"in document||"mozPointerLockElement"in document};d.isopera="opera"in window;d.isopera12=d.isopera&&"getUserMedia"in navigator;d.isoperamini="[object OperaMini]"===Object.prototype.toString.call(window.operamini);d.isie="all"in document&&"attachEvent"in f&&!d.isopera; +d.isieold=d.isie&&!("msInterpolationMode"in c);d.isie7=d.isie&&!d.isieold&&(!("documentMode"in document)||7==document.documentMode);d.isie8=d.isie&&"documentMode"in document&&8==document.documentMode;d.isie9=d.isie&&"performance"in window&&9<=document.documentMode;d.isie10=d.isie&&"performance"in window&&10==document.documentMode;d.isie11="msRequestFullscreen"in f&&11<=document.documentMode;d.isie9mobile=/iemobile.9/i.test(h);d.isie9mobile&&(d.isie9=!1);d.isie7mobile=!d.isie9mobile&&d.isie7&&/iemobile/i.test(h); +d.ismozilla="MozAppearance"in c;d.iswebkit="WebkitAppearance"in c;d.ischrome="chrome"in window;d.ischrome22=d.ischrome&&d.haspointerlock;d.ischrome26=d.ischrome&&"transition"in c;d.cantouch="ontouchstart"in document.documentElement||"ontouchstart"in window;d.hasmstouch=window.MSPointerEvent||!1;d.hasw3ctouch=window.PointerEvent||!1;d.ismac=/^mac$/i.test(m);d.isios=d.cantouch&&/iphone|ipad|ipod/i.test(m);d.isios4=d.isios&&!("seal"in Object);d.isios7=d.isios&&"webkitHidden"in document;d.isandroid=/android/i.test(h); +d.haseventlistener="addEventListener"in f;d.trstyle=!1;d.hastransform=!1;d.hastranslate3d=!1;d.transitionstyle=!1;d.hastransition=!1;d.transitionend=!1;m=["transform","msTransform","webkitTransform","MozTransform","OTransform"];for(h=0;h<m.length;h++)if("undefined"!=typeof c[m[h]]){d.trstyle=m[h];break}d.hastransform=!!d.trstyle;d.hastransform&&(c[d.trstyle]="translate3d(1px,2px,3px)",d.hastranslate3d=/translate3d/.test(c[d.trstyle]));d.transitionstyle=!1;d.prefixstyle="";d.transitionend=!1;for(var m= +"transition webkitTransition msTransition MozTransition OTransition OTransition KhtmlTransition".split(" "),n=" -webkit- -ms- -moz- -o- -o -khtml-".split(" "),p="transitionend webkitTransitionEnd msTransitionEnd transitionend otransitionend oTransitionEnd KhtmlTransitionEnd".split(" "),h=0;h<m.length;h++)if(m[h]in c){d.transitionstyle=m[h];d.prefixstyle=n[h];d.transitionend=p[h];break}d.ischrome26&&(d.prefixstyle=n[1]);d.hastransition=d.transitionstyle;a:{h=["-webkit-grab","-moz-grab","grab"];if(d.ischrome&& +!d.ischrome22||d.isie)h=[];for(m=0;m<h.length;m++)if(n=h[m],c.cursor=n,c.cursor==n){c=n;break a}c="url(//mail.google.com/mail/images/2/openhand.cur),n-resize"}d.cursorgrabvalue=c;d.hasmousecapture="setCapture"in f;d.hasMutationObserver=!1!==v;return F=d},R=function(k,c){function h(){var b=a.doc.css(e.trstyle);return b&&"matrix"==b.substr(0,6)?b.replace(/^.*\((.*)\)$/g,"$1").replace(/px/g,"").split(/, +/):!1}function m(){var b=a.win;if("zIndex"in b)return b.zIndex();for(;0<b.length&&9!=b[0].nodeType;){var g= +b.css("zIndex");if(!isNaN(g)&&0!=g)return parseInt(g);b=b.parent()}return!1}function d(b,g,q){g=b.css(g);b=parseFloat(g);return isNaN(b)?(b=w[g]||0,q=3==b?q?a.win.outerHeight()-a.win.innerHeight():a.win.outerWidth()-a.win.innerWidth():1,a.isie8&&b&&(b+=1),q?b:0):b}function n(b,g,q,c){a._bind(b,g,function(a){a=a?a:window.event;var c={original:a,target:a.target||a.srcElement,type:"wheel",deltaMode:"MozMousePixelScroll"==a.type?0:1,deltaX:0,deltaZ:0,preventDefault:function(){a.preventDefault?a.preventDefault(): +a.returnValue=!1;return!1},stopImmediatePropagation:function(){a.stopImmediatePropagation?a.stopImmediatePropagation():a.cancelBubble=!0}};"mousewheel"==g?(c.deltaY=-.025*a.wheelDelta,a.wheelDeltaX&&(c.deltaX=-.025*a.wheelDeltaX)):c.deltaY=a.detail;return q.call(b,c)},c)}function p(b,g,c){var d,e;0==b.deltaMode?(d=-Math.floor(a.opt.mousescrollstep/54*b.deltaX),e=-Math.floor(a.opt.mousescrollstep/54*b.deltaY)):1==b.deltaMode&&(d=-Math.floor(b.deltaX*a.opt.mousescrollstep),e=-Math.floor(b.deltaY*a.opt.mousescrollstep)); +g&&a.opt.oneaxismousemode&&0==d&&e&&(d=e,e=0,c&&(0>d?a.getScrollLeft()>=a.page.maxw:0>=a.getScrollLeft())&&(e=d,d=0));d&&(a.scrollmom&&a.scrollmom.stop(),a.lastdeltax+=d,a.debounced("mousewheelx",function(){var b=a.lastdeltax;a.lastdeltax=0;a.rail.drag||a.doScrollLeftBy(b)},15));if(e){if(a.opt.nativeparentscrolling&&c&&!a.ispage&&!a.zoomactive)if(0>e){if(a.getScrollTop()>=a.page.maxh)return!0}else if(0>=a.getScrollTop())return!0;a.scrollmom&&a.scrollmom.stop();a.lastdeltay+=e;a.debounced("mousewheely", +function(){var b=a.lastdeltay;a.lastdeltay=0;a.rail.drag||a.doScrollBy(b)},15)}b.stopImmediatePropagation();return b.preventDefault()}var a=this;this.version="3.6.0";this.name="nicescroll";this.me=c;this.opt={doc:f("body"),win:!1};f.extend(this.opt,I);this.opt.snapbackspeed=80;if(k)for(var G in a.opt)"undefined"!=typeof k[G]&&(a.opt[G]=k[G]);this.iddoc=(this.doc=a.opt.doc)&&this.doc[0]?this.doc[0].id||"":"";this.ispage=/^BODY|HTML/.test(a.opt.win?a.opt.win[0].nodeName:this.doc[0].nodeName);this.haswrapper= +!1!==a.opt.win;this.win=a.opt.win||(this.ispage?f(window):this.doc);this.docscroll=this.ispage&&!this.haswrapper?f(window):this.win;this.body=f("body");this.iframe=this.isfixed=this.viewport=!1;this.isiframe="IFRAME"==this.doc[0].nodeName&&"IFRAME"==this.win[0].nodeName;this.istextarea="TEXTAREA"==this.win[0].nodeName;this.forcescreen=!1;this.canshowonmouseevent="scroll"!=a.opt.autohidemode;this.page=this.view=this.onzoomout=this.onzoomin=this.onscrollcancel=this.onscrollend=this.onscrollstart=this.onclick= +this.ongesturezoom=this.onkeypress=this.onmousewheel=this.onmousemove=this.onmouseup=this.onmousedown=!1;this.scroll={x:0,y:0};this.scrollratio={x:0,y:0};this.cursorheight=20;this.scrollvaluemax=0;this.isrtlmode="auto"==this.opt.rtlmode?"rtl"==(this.win[0]==window?this.body:this.win).css("direction"):!0===this.opt.rtlmode;this.observerbody=this.observerremover=this.observer=this.scrollmom=this.scrollrunning=!1;do this.id="ascrail"+O++;while(document.getElementById(this.id));this.hasmousefocus=this.hasfocus= +this.zoomactive=this.zoom=this.selectiondrag=this.cursorfreezed=this.cursor=this.rail=!1;this.visibility=!0;this.hidden=this.locked=this.railslocked=!1;this.cursoractive=!0;this.wheelprevented=!1;this.overflowx=a.opt.overflowx;this.overflowy=a.opt.overflowy;this.nativescrollingarea=!1;this.checkarea=0;this.events=[];this.saved={};this.delaylist={};this.synclist={};this.lastdeltay=this.lastdeltax=0;this.detected=Q();var e=f.extend({},this.detected);this.ishwscroll=(this.canhwscroll=e.hastransform&& +a.opt.hwacceleration)&&a.haswrapper;this.hasreversehr=this.isrtlmode&&!e.iswebkit;this.istouchcapable=!1;!e.cantouch||e.isios||e.isandroid||!e.iswebkit&&!e.ismozilla||(this.istouchcapable=!0,e.cantouch=!1);a.opt.enablemouselockapi||(e.hasmousecapture=!1,e.haspointerlock=!1);this.debounced=function(b,g,c){var d=a.delaylist[b];a.delaylist[b]=g;d||setTimeout(function(){var g=a.delaylist[b];a.delaylist[b]=!1;g.call(a)},c)};var r=!1;this.synched=function(b,g){a.synclist[b]=g;(function(){r||(s(function(){r= +!1;for(var b in a.synclist){var g=a.synclist[b];g&&g.call(a);a.synclist[b]=!1}}),r=!0)})();return b};this.unsynched=function(b){a.synclist[b]&&(a.synclist[b]=!1)};this.css=function(b,g){for(var c in g)a.saved.css.push([b,c,b.css(c)]),b.css(c,g[c])};this.scrollTop=function(b){return"undefined"==typeof b?a.getScrollTop():a.setScrollTop(b)};this.scrollLeft=function(b){return"undefined"==typeof b?a.getScrollLeft():a.setScrollLeft(b)};var A=function(a,g,c,d,e,f,h){this.st=a;this.ed=g;this.spd=c;this.p1= +d||0;this.p2=e||1;this.p3=f||0;this.p4=h||1;this.ts=(new Date).getTime();this.df=this.ed-this.st};A.prototype={B2:function(a){return 3*a*a*(1-a)},B3:function(a){return 3*a*(1-a)*(1-a)},B4:function(a){return(1-a)*(1-a)*(1-a)},getNow:function(){var a=1-((new Date).getTime()-this.ts)/this.spd,g=this.B2(a)+this.B3(a)+this.B4(a);return 0>a?this.ed:this.st+Math.round(this.df*g)},update:function(a,g){this.st=this.getNow();this.ed=a;this.spd=g;this.ts=(new Date).getTime();this.df=this.ed-this.st;return this}}; +if(this.ishwscroll){this.doc.translate={x:0,y:0,tx:"0px",ty:"0px"};e.hastranslate3d&&e.isios&&this.doc.css("-webkit-backface-visibility","hidden");this.getScrollTop=function(b){if(!b){if(b=h())return 16==b.length?-b[13]:-b[5];if(a.timerscroll&&a.timerscroll.bz)return a.timerscroll.bz.getNow()}return a.doc.translate.y};this.getScrollLeft=function(b){if(!b){if(b=h())return 16==b.length?-b[12]:-b[4];if(a.timerscroll&&a.timerscroll.bh)return a.timerscroll.bh.getNow()}return a.doc.translate.x};this.notifyScrollEvent= +function(a){var g=document.createEvent("UIEvents");g.initUIEvent("scroll",!1,!0,window,1);g.niceevent=!0;a.dispatchEvent(g)};var K=this.isrtlmode?1:-1;e.hastranslate3d&&a.opt.enabletranslate3d?(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate3d("+a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate3d("+ +a.doc.translate.tx+","+a.doc.translate.ty+",0px)");g||a.notifyScrollEvent(a.win[0])}):(this.setScrollTop=function(b,g){a.doc.translate.y=b;a.doc.translate.ty=-1*b+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])},this.setScrollLeft=function(b,g){a.doc.translate.x=b;a.doc.translate.tx=b*K+"px";a.doc.css(e.trstyle,"translate("+a.doc.translate.tx+","+a.doc.translate.ty+")");g||a.notifyScrollEvent(a.win[0])})}else this.getScrollTop= +function(){return a.docscroll.scrollTop()},this.setScrollTop=function(b){return a.docscroll.scrollTop(b)},this.getScrollLeft=function(){return a.detected.ismozilla&&a.isrtlmode?Math.abs(a.docscroll.scrollLeft()):a.docscroll.scrollLeft()},this.setScrollLeft=function(b){return a.docscroll.scrollLeft(a.detected.ismozilla&&a.isrtlmode?-b:b)};this.getTarget=function(a){return a?a.target?a.target:a.srcElement?a.srcElement:!1:!1};this.hasParent=function(a,g){if(!a)return!1;for(var c=a.target||a.srcElement|| +a||!1;c&&c.id!=g;)c=c.parentNode||!1;return!1!==c};var w={thin:1,medium:3,thick:5};this.getDocumentScrollOffset=function(){return{top:window.pageYOffset||document.documentElement.scrollTop,left:window.pageXOffset||document.documentElement.scrollLeft}};this.getOffset=function(){if(a.isfixed){var b=a.win.offset(),g=a.getDocumentScrollOffset();b.top-=g.top;b.left-=g.left;return b}b=a.win.offset();if(!a.viewport)return b;g=a.viewport.offset();return{top:b.top-g.top,left:b.left-g.left}};this.updateScrollBar= +function(b){if(a.ishwscroll)a.rail.css({height:a.win.innerHeight()-(a.opt.railpadding.top+a.opt.railpadding.bottom)}),a.railh&&a.railh.css({width:a.win.innerWidth()-(a.opt.railpadding.left+a.opt.railpadding.right)});else{var g=a.getOffset(),c=g.top,e=g.left-(a.opt.railpadding.left+a.opt.railpadding.right),c=c+d(a.win,"border-top-width",!0),e=e+(a.rail.align?a.win.outerWidth()-d(a.win,"border-right-width")-a.rail.width:d(a.win,"border-left-width")),f=a.opt.railoffset;f&&(f.top&&(c+=f.top),a.rail.align&& +f.left&&(e+=f.left));a.railslocked||a.rail.css({top:c,left:e,height:(b?b.h:a.win.innerHeight())-(a.opt.railpadding.top+a.opt.railpadding.bottom)});a.zoom&&a.zoom.css({top:c+1,left:1==a.rail.align?e-20:e+a.rail.width+4});if(a.railh&&!a.railslocked){c=g.top;e=g.left;if(f=a.opt.railhoffset)f.top&&(c+=f.top),f.left&&(e+=f.left);b=a.railh.align?c+d(a.win,"border-top-width",!0)+a.win.innerHeight()-a.railh.height:c+d(a.win,"border-top-width",!0);e+=d(a.win,"border-left-width");a.railh.css({top:b-(a.opt.railpadding.top+ +a.opt.railpadding.bottom),left:e,width:a.railh.width})}}};this.doRailClick=function(b,g,c){var e;a.railslocked||(a.cancelEvent(b),g?(g=c?a.doScrollLeft:a.doScrollTop,e=c?(b.pageX-a.railh.offset().left-a.cursorwidth/2)*a.scrollratio.x:(b.pageY-a.rail.offset().top-a.cursorheight/2)*a.scrollratio.y,g(e)):(g=c?a.doScrollLeftBy:a.doScrollBy,e=c?a.scroll.x:a.scroll.y,b=c?b.pageX-a.railh.offset().left:b.pageY-a.rail.offset().top,c=c?a.view.w:a.view.h,g(e>=b?c:-c)))};a.hasanimationframe=s;a.hascancelanimationframe= +t;a.hasanimationframe?a.hascancelanimationframe||(t=function(){a.cancelAnimationFrame=!0}):(s=function(a){return setTimeout(a,15-Math.floor(+new Date/1E3)%16)},t=clearInterval);this.init=function(){a.saved.css=[];if(e.isie7mobile||e.isoperamini)return!0;e.hasmstouch&&a.css(a.ispage?f("html"):a.win,{"-ms-touch-action":"none"});a.zindex="auto";a.zindex=a.ispage||"auto"!=a.opt.zindex?a.opt.zindex:m()||"auto";!a.ispage&&"auto"!=a.zindex&&a.zindex>x&&(x=a.zindex);a.isie&&0==a.zindex&&"auto"==a.opt.zindex&& +(a.zindex="auto");if(!a.ispage||!e.cantouch&&!e.isieold&&!e.isie9mobile){var b=a.docscroll;a.ispage&&(b=a.haswrapper?a.win:a.doc);e.isie9mobile||a.css(b,{"overflow-y":"hidden"});a.ispage&&e.isie7&&("BODY"==a.doc[0].nodeName?a.css(f("html"),{"overflow-y":"hidden"}):"HTML"==a.doc[0].nodeName&&a.css(f("body"),{"overflow-y":"hidden"}));!e.isios||a.ispage||a.haswrapper||a.css(f("body"),{"-webkit-overflow-scrolling":"touch"});var g=f(document.createElement("div"));g.css({position:"relative",top:0,"float":"right", +width:a.opt.cursorwidth,height:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius});g.hborder=parseFloat(g.outerHeight()-g.innerHeight());g.addClass("nicescroll-cursors");a.cursor=g;var c=f(document.createElement("div"));c.attr("id",a.id);c.addClass("nicescroll-rails nicescroll-rails-vr");var d,h,k=["left","right", +"top","bottom"],J;for(J in k)h=k[J],(d=a.opt.railpadding[h])?c.css("padding-"+h,d+"px"):a.opt.railpadding[h]=0;c.append(g);c.width=Math.max(parseFloat(a.opt.cursorwidth),g.outerWidth());c.css({width:c.width+"px",zIndex:a.zindex,background:a.opt.background,cursor:"default"});c.visibility=!0;c.scrollable=!0;c.align="left"==a.opt.railalign?0:1;a.rail=c;g=a.rail.drag=!1;!a.opt.boxzoom||a.ispage||e.isieold||(g=document.createElement("div"),a.bind(g,"click",a.doZoom),a.bind(g,"mouseenter",function(){a.zoom.css("opacity", +a.opt.cursoropacitymax)}),a.bind(g,"mouseleave",function(){a.zoom.css("opacity",a.opt.cursoropacitymin)}),a.zoom=f(g),a.zoom.css({cursor:"pointer","z-index":a.zindex,backgroundImage:"url("+a.opt.scriptpath+"zoomico.png)",height:18,width:18,backgroundPosition:"0px 0px"}),a.opt.dblclickzoom&&a.bind(a.win,"dblclick",a.doZoom),e.cantouch&&a.opt.gesturezoom&&(a.ongesturezoom=function(b){1.5<b.scale&&a.doZoomIn(b);.8>b.scale&&a.doZoomOut(b);return a.cancelEvent(b)},a.bind(a.win,"gestureend",a.ongesturezoom))); +a.railh=!1;var l;a.opt.horizrailenabled&&(a.css(b,{"overflow-x":"hidden"}),g=f(document.createElement("div")),g.css({position:"absolute",top:0,height:a.opt.cursorwidth,width:"0px","background-color":a.opt.cursorcolor,border:a.opt.cursorborder,"background-clip":"padding-box","-webkit-border-radius":a.opt.cursorborderradius,"-moz-border-radius":a.opt.cursorborderradius,"border-radius":a.opt.cursorborderradius}),e.isieold&&g.css({overflow:"hidden"}),g.wborder=parseFloat(g.outerWidth()-g.innerWidth()), +g.addClass("nicescroll-cursors"),a.cursorh=g,l=f(document.createElement("div")),l.attr("id",a.id+"-hr"),l.addClass("nicescroll-rails nicescroll-rails-hr"),l.height=Math.max(parseFloat(a.opt.cursorwidth),g.outerHeight()),l.css({height:l.height+"px",zIndex:a.zindex,background:a.opt.background}),l.append(g),l.visibility=!0,l.scrollable=!0,l.align="top"==a.opt.railvalign?0:1,a.railh=l,a.railh.drag=!1);a.ispage?(c.css({position:"fixed",top:"0px",height:"100%"}),c.align?c.css({right:"0px"}):c.css({left:"0px"}), +a.body.append(c),a.railh&&(l.css({position:"fixed",left:"0px",width:"100%"}),l.align?l.css({bottom:"0px"}):l.css({top:"0px"}),a.body.append(l))):(a.ishwscroll?("static"==a.win.css("position")&&a.css(a.win,{position:"relative"}),b="HTML"==a.win[0].nodeName?a.body:a.win,f(b).scrollTop(0).scrollLeft(0),a.zoom&&(a.zoom.css({position:"absolute",top:1,right:0,"margin-right":c.width+4}),b.append(a.zoom)),c.css({position:"absolute",top:0}),c.align?c.css({right:0}):c.css({left:0}),b.append(c),l&&(l.css({position:"absolute", +left:0,bottom:0}),l.align?l.css({bottom:0}):l.css({top:0}),b.append(l))):(a.isfixed="fixed"==a.win.css("position"),b=a.isfixed?"fixed":"absolute",a.isfixed||(a.viewport=a.getViewport(a.win[0])),a.viewport&&(a.body=a.viewport,0==/fixed|absolute/.test(a.viewport.css("position"))&&a.css(a.viewport,{position:"relative"})),c.css({position:b}),a.zoom&&a.zoom.css({position:b}),a.updateScrollBar(),a.body.append(c),a.zoom&&a.body.append(a.zoom),a.railh&&(l.css({position:b}),a.body.append(l))),e.isios&&a.css(a.win, +{"-webkit-tap-highlight-color":"rgba(0,0,0,0)","-webkit-touch-callout":"none"}),e.isie&&a.opt.disableoutline&&a.win.attr("hideFocus","true"),e.iswebkit&&a.opt.disableoutline&&a.win.css({outline:"none"}));!1===a.opt.autohidemode?(a.autohidedom=!1,a.rail.css({opacity:a.opt.cursoropacitymax}),a.railh&&a.railh.css({opacity:a.opt.cursoropacitymax})):!0===a.opt.autohidemode||"leave"===a.opt.autohidemode?(a.autohidedom=f().add(a.rail),e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursor)),a.railh&&(a.autohidedom= +a.autohidedom.add(a.railh)),a.railh&&e.isie8&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"scroll"==a.opt.autohidemode?(a.autohidedom=f().add(a.rail),a.railh&&(a.autohidedom=a.autohidedom.add(a.railh))):"cursor"==a.opt.autohidemode?(a.autohidedom=f().add(a.cursor),a.railh&&(a.autohidedom=a.autohidedom.add(a.cursorh))):"hidden"==a.opt.autohidemode&&(a.autohidedom=!1,a.hide(),a.railslocked=!1);if(e.isie9mobile)a.scrollmom=new L(a),a.onmangotouch=function(){var b=a.getScrollTop(),c=a.getScrollLeft(); +if(b==a.scrollmom.lastscrolly&&c==a.scrollmom.lastscrollx)return!0;var g=b-a.mangotouch.sy,e=c-a.mangotouch.sx;if(0!=Math.round(Math.sqrt(Math.pow(e,2)+Math.pow(g,2)))){var d=0>g?-1:1,f=0>e?-1:1,q=+new Date;a.mangotouch.lazy&&clearTimeout(a.mangotouch.lazy);80<q-a.mangotouch.tm||a.mangotouch.dry!=d||a.mangotouch.drx!=f?(a.scrollmom.stop(),a.scrollmom.reset(c,b),a.mangotouch.sy=b,a.mangotouch.ly=b,a.mangotouch.sx=c,a.mangotouch.lx=c,a.mangotouch.dry=d,a.mangotouch.drx=f,a.mangotouch.tm=q):(a.scrollmom.stop(), +a.scrollmom.update(a.mangotouch.sx-e,a.mangotouch.sy-g),a.mangotouch.tm=q,g=Math.max(Math.abs(a.mangotouch.ly-b),Math.abs(a.mangotouch.lx-c)),a.mangotouch.ly=b,a.mangotouch.lx=c,2<g&&(a.mangotouch.lazy=setTimeout(function(){a.mangotouch.lazy=!1;a.mangotouch.dry=0;a.mangotouch.drx=0;a.mangotouch.tm=0;a.scrollmom.doMomentum(30)},100)))}},c=a.getScrollTop(),l=a.getScrollLeft(),a.mangotouch={sy:c,ly:c,dry:0,sx:l,lx:l,drx:0,lazy:!1,tm:0},a.bind(a.docscroll,"scroll",a.onmangotouch);else{if(e.cantouch|| +a.istouchcapable||a.opt.touchbehavior||e.hasmstouch){a.scrollmom=new L(a);a.ontouchstart=function(b){if(b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1;a.hasmoving=!1;if(!a.railslocked){var c;if(e.hasmstouch)for(c=b.target?b.target:!1;c;){var g=f(c).getNiceScroll();if(0<g.length&&g[0].me==a.me)break;if(0<g.length)return!1;if("DIV"==c.nodeName&&c.id==a.id)break;c=c.parentNode?c.parentNode:!1}a.cancelScroll();if((c=a.getTarget(b))&&/INPUT/i.test(c.nodeName)&&/range/i.test(c.type))return a.stopPropagation(b); +!("clientX"in b)&&"changedTouches"in b&&(b.clientX=b.changedTouches[0].clientX,b.clientY=b.changedTouches[0].clientY);a.forcescreen&&(g=b,b={original:b.original?b.original:b},b.clientX=g.screenX,b.clientY=g.screenY);a.rail.drag={x:b.clientX,y:b.clientY,sx:a.scroll.x,sy:a.scroll.y,st:a.getScrollTop(),sl:a.getScrollLeft(),pt:2,dl:!1};if(a.ispage||!a.opt.directionlockdeadzone)a.rail.drag.dl="f";else{var g=f(window).width(),d=f(window).height(),q=Math.max(document.body.scrollWidth,document.documentElement.scrollWidth), +h=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight),d=Math.max(0,h-d),g=Math.max(0,q-g);a.rail.drag.ck=!a.rail.scrollable&&a.railh.scrollable?0<d?"v":!1:a.rail.scrollable&&!a.railh.scrollable?0<g?"h":!1:!1;a.rail.drag.ck||(a.rail.drag.dl="f")}a.opt.touchbehavior&&a.isiframe&&e.isie&&(g=a.win.position(),a.rail.drag.x+=g.left,a.rail.drag.y+=g.top);a.hasmoving=!1;a.lastmouseup=!1;a.scrollmom.reset(b.clientX,b.clientY);if(!e.cantouch&&!this.istouchcapable&&!b.pointerType){if(!c|| +!/INPUT|SELECT|TEXTAREA/i.test(c.nodeName))return!a.ispage&&e.hasmousecapture&&c.setCapture(),a.opt.touchbehavior?(c.onclick&&!c._onclick&&(c._onclick=c.onclick,c.onclick=function(b){if(a.hasmoving)return!1;c._onclick.call(this,b)}),a.cancelEvent(b)):a.stopPropagation(b);/SUBMIT|CANCEL|BUTTON/i.test(f(c).attr("type"))&&(pc={tg:c,click:!1},a.preventclick=pc)}}};a.ontouchend=function(b){if(!a.rail.drag)return!0;if(2==a.rail.drag.pt){if(b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1; +a.scrollmom.doMomentum();a.rail.drag=!1;if(a.hasmoving&&(a.lastmouseup=!0,a.hideCursor(),e.hasmousecapture&&document.releaseCapture(),!e.cantouch))return a.cancelEvent(b)}else if(1==a.rail.drag.pt)return a.onmouseup(b)};var n=a.opt.touchbehavior&&a.isiframe&&!e.hasmousecapture;a.ontouchmove=function(b,c){if(!a.rail.drag||b.targetTouches&&a.opt.preventmultitouchscrolling&&1<b.targetTouches.length||b.pointerType&&2!=b.pointerType&&"touch"!=b.pointerType)return!1;if(2==a.rail.drag.pt){if(e.cantouch&& +e.isios&&"undefined"==typeof b.original)return!0;a.hasmoving=!0;a.preventclick&&!a.preventclick.click&&(a.preventclick.click=a.preventclick.tg.onclick||!1,a.preventclick.tg.onclick=a.onpreventclick);b=f.extend({original:b},b);"changedTouches"in b&&(b.clientX=b.changedTouches[0].clientX,b.clientY=b.changedTouches[0].clientY);if(a.forcescreen){var g=b;b={original:b.original?b.original:b};b.clientX=g.screenX;b.clientY=g.screenY}var d,g=d=0;n&&!c&&(d=a.win.position(),g=-d.left,d=-d.top);var q=b.clientY+ +d;d=q-a.rail.drag.y;var h=b.clientX+g,u=h-a.rail.drag.x,k=a.rail.drag.st-d;a.ishwscroll&&a.opt.bouncescroll?0>k?k=Math.round(k/2):k>a.page.maxh&&(k=a.page.maxh+Math.round((k-a.page.maxh)/2)):(0>k&&(q=k=0),k>a.page.maxh&&(k=a.page.maxh,q=0));var l;a.railh&&a.railh.scrollable&&(l=a.isrtlmode?u-a.rail.drag.sl:a.rail.drag.sl-u,a.ishwscroll&&a.opt.bouncescroll?0>l?l=Math.round(l/2):l>a.page.maxw&&(l=a.page.maxw+Math.round((l-a.page.maxw)/2)):(0>l&&(h=l=0),l>a.page.maxw&&(l=a.page.maxw,h=0)));g=!1;if(a.rail.drag.dl)g= +!0,"v"==a.rail.drag.dl?l=a.rail.drag.sl:"h"==a.rail.drag.dl&&(k=a.rail.drag.st);else{d=Math.abs(d);var u=Math.abs(u),z=a.opt.directionlockdeadzone;if("v"==a.rail.drag.ck){if(d>z&&u<=.3*d)return a.rail.drag=!1,!0;u>z&&(a.rail.drag.dl="f",f("body").scrollTop(f("body").scrollTop()))}else if("h"==a.rail.drag.ck){if(u>z&&d<=.3*u)return a.rail.drag=!1,!0;d>z&&(a.rail.drag.dl="f",f("body").scrollLeft(f("body").scrollLeft()))}}a.synched("touchmove",function(){a.rail.drag&&2==a.rail.drag.pt&&(a.prepareTransition&& +a.prepareTransition(0),a.rail.scrollable&&a.setScrollTop(k),a.scrollmom.update(h,q),a.railh&&a.railh.scrollable?(a.setScrollLeft(l),a.showCursor(k,l)):a.showCursor(k),e.isie10&&document.selection.clear())});e.ischrome&&a.istouchcapable&&(g=!1);if(g)return a.cancelEvent(b)}else if(1==a.rail.drag.pt)return a.onmousemove(b)}}a.onmousedown=function(b,c){if(!a.rail.drag||1==a.rail.drag.pt){if(a.railslocked)return a.cancelEvent(b);a.cancelScroll();a.rail.drag={x:b.clientX,y:b.clientY,sx:a.scroll.x,sy:a.scroll.y, +pt:1,hr:!!c};var g=a.getTarget(b);!a.ispage&&e.hasmousecapture&&g.setCapture();a.isiframe&&!e.hasmousecapture&&(a.saved.csspointerevents=a.doc.css("pointer-events"),a.css(a.doc,{"pointer-events":"none"}));a.hasmoving=!1;return a.cancelEvent(b)}};a.onmouseup=function(b){if(a.rail.drag){if(1!=a.rail.drag.pt)return!0;e.hasmousecapture&&document.releaseCapture();a.isiframe&&!e.hasmousecapture&&a.doc.css("pointer-events",a.saved.csspointerevents);a.rail.drag=!1;a.hasmoving&&a.triggerScrollEnd();return a.cancelEvent(b)}}; +a.onmousemove=function(b){if(a.rail.drag&&1==a.rail.drag.pt){if(e.ischrome&&0==b.which)return a.onmouseup(b);a.cursorfreezed=!0;a.hasmoving=!0;if(a.rail.drag.hr){a.scroll.x=a.rail.drag.sx+(b.clientX-a.rail.drag.x);0>a.scroll.x&&(a.scroll.x=0);var c=a.scrollvaluemaxw;a.scroll.x>c&&(a.scroll.x=c)}else a.scroll.y=a.rail.drag.sy+(b.clientY-a.rail.drag.y),0>a.scroll.y&&(a.scroll.y=0),c=a.scrollvaluemax,a.scroll.y>c&&(a.scroll.y=c);a.synched("mousemove",function(){a.rail.drag&&1==a.rail.drag.pt&&(a.showCursor(), +a.rail.drag.hr?a.hasreversehr?a.doScrollLeft(a.scrollvaluemaxw-Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollLeft(Math.round(a.scroll.x*a.scrollratio.x),a.opt.cursordragspeed):a.doScrollTop(Math.round(a.scroll.y*a.scrollratio.y),a.opt.cursordragspeed))});return a.cancelEvent(b)}};if(e.cantouch||a.opt.touchbehavior)a.onpreventclick=function(b){if(a.preventclick)return a.preventclick.tg.onclick=a.preventclick.click,a.preventclick=!1,a.cancelEvent(b)},a.bind(a.win,"mousedown", +a.ontouchstart),a.onclick=e.isios?!1:function(b){return a.lastmouseup?(a.lastmouseup=!1,a.cancelEvent(b)):!0},a.opt.grabcursorenabled&&e.cursorgrabvalue&&(a.css(a.ispage?a.doc:a.win,{cursor:e.cursorgrabvalue}),a.css(a.rail,{cursor:e.cursorgrabvalue}));else{var p=function(b){if(a.selectiondrag){if(b){var c=a.win.outerHeight();b=b.pageY-a.selectiondrag.top;0<b&&b<c&&(b=0);b>=c&&(b-=c);a.selectiondrag.df=b}0!=a.selectiondrag.df&&(a.doScrollBy(2*-Math.floor(a.selectiondrag.df/6)),a.debounced("doselectionscroll", +function(){p()},50))}};a.hasTextSelected="getSelection"in document?function(){return 0<document.getSelection().rangeCount}:"selection"in document?function(){return"None"!=document.selection.type}:function(){return!1};a.onselectionstart=function(b){a.ispage||(a.selectiondrag=a.win.offset())};a.onselectionend=function(b){a.selectiondrag=!1};a.onselectiondrag=function(b){a.selectiondrag&&a.hasTextSelected()&&a.debounced("selectionscroll",function(){p(b)},250)}}e.hasw3ctouch?(a.css(a.rail,{"touch-action":"none"}), +a.css(a.cursor,{"touch-action":"none"}),a.bind(a.win,"pointerdown",a.ontouchstart),a.bind(document,"pointerup",a.ontouchend),a.bind(document,"pointermove",a.ontouchmove)):e.hasmstouch?(a.css(a.rail,{"-ms-touch-action":"none"}),a.css(a.cursor,{"-ms-touch-action":"none"}),a.bind(a.win,"MSPointerDown",a.ontouchstart),a.bind(document,"MSPointerUp",a.ontouchend),a.bind(document,"MSPointerMove",a.ontouchmove),a.bind(a.cursor,"MSGestureHold",function(a){a.preventDefault()}),a.bind(a.cursor,"contextmenu", +function(a){a.preventDefault()})):this.istouchcapable&&(a.bind(a.win,"touchstart",a.ontouchstart),a.bind(document,"touchend",a.ontouchend),a.bind(document,"touchcancel",a.ontouchend),a.bind(document,"touchmove",a.ontouchmove));if(a.opt.cursordragontouch||!e.cantouch&&!a.opt.touchbehavior)a.rail.css({cursor:"default"}),a.railh&&a.railh.css({cursor:"default"}),a.jqbind(a.rail,"mouseenter",function(){if(!a.ispage&&!a.win.is(":visible"))return!1;a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}), +a.jqbind(a.rail,"mouseleave",function(){a.rail.active=!1;a.rail.drag||a.hideCursor()}),a.opt.sensitiverail&&(a.bind(a.rail,"click",function(b){a.doRailClick(b,!1,!1)}),a.bind(a.rail,"dblclick",function(b){a.doRailClick(b,!0,!1)}),a.bind(a.cursor,"click",function(b){a.cancelEvent(b)}),a.bind(a.cursor,"dblclick",function(b){a.cancelEvent(b)})),a.railh&&(a.jqbind(a.railh,"mouseenter",function(){if(!a.ispage&&!a.win.is(":visible"))return!1;a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}),a.jqbind(a.railh, +"mouseleave",function(){a.rail.active=!1;a.rail.drag||a.hideCursor()}),a.opt.sensitiverail&&(a.bind(a.railh,"click",function(b){a.doRailClick(b,!1,!0)}),a.bind(a.railh,"dblclick",function(b){a.doRailClick(b,!0,!0)}),a.bind(a.cursorh,"click",function(b){a.cancelEvent(b)}),a.bind(a.cursorh,"dblclick",function(b){a.cancelEvent(b)})));e.cantouch||a.opt.touchbehavior?(a.bind(e.hasmousecapture?a.win:document,"mouseup",a.ontouchend),a.bind(document,"mousemove",a.ontouchmove),a.onclick&&a.bind(document,"click", +a.onclick),a.opt.cursordragontouch&&(a.bind(a.cursor,"mousedown",a.onmousedown),a.bind(a.cursor,"mouseup",a.onmouseup),a.cursorh&&a.bind(a.cursorh,"mousedown",function(b){a.onmousedown(b,!0)}),a.cursorh&&a.bind(a.cursorh,"mouseup",a.onmouseup))):(a.bind(e.hasmousecapture?a.win:document,"mouseup",a.onmouseup),a.bind(document,"mousemove",a.onmousemove),a.onclick&&a.bind(document,"click",a.onclick),a.bind(a.cursor,"mousedown",a.onmousedown),a.bind(a.cursor,"mouseup",a.onmouseup),a.railh&&(a.bind(a.cursorh, +"mousedown",function(b){a.onmousedown(b,!0)}),a.bind(a.cursorh,"mouseup",a.onmouseup)),!a.ispage&&a.opt.enablescrollonselection&&(a.bind(a.win[0],"mousedown",a.onselectionstart),a.bind(document,"mouseup",a.onselectionend),a.bind(a.cursor,"mouseup",a.onselectionend),a.cursorh&&a.bind(a.cursorh,"mouseup",a.onselectionend),a.bind(document,"mousemove",a.onselectiondrag)),a.zoom&&(a.jqbind(a.zoom,"mouseenter",function(){a.canshowonmouseevent&&a.showCursor();a.rail.active=!0}),a.jqbind(a.zoom,"mouseleave", +function(){a.rail.active=!1;a.rail.drag||a.hideCursor()})));a.opt.enablemousewheel&&(a.isiframe||a.bind(e.isie&&a.ispage?document:a.win,"mousewheel",a.onmousewheel),a.bind(a.rail,"mousewheel",a.onmousewheel),a.railh&&a.bind(a.railh,"mousewheel",a.onmousewheelhr));a.ispage||e.cantouch||/HTML|^BODY/.test(a.win[0].nodeName)||(a.win.attr("tabindex")||a.win.attr({tabindex:N++}),a.jqbind(a.win,"focus",function(b){y=a.getTarget(b).id||!0;a.hasfocus=!0;a.canshowonmouseevent&&a.noticeCursor()}),a.jqbind(a.win, +"blur",function(b){y=!1;a.hasfocus=!1}),a.jqbind(a.win,"mouseenter",function(b){D=a.getTarget(b).id||!0;a.hasmousefocus=!0;a.canshowonmouseevent&&a.noticeCursor()}),a.jqbind(a.win,"mouseleave",function(){D=!1;a.hasmousefocus=!1;a.rail.drag||a.hideCursor()}))}a.onkeypress=function(b){if(a.railslocked&&0==a.page.maxh)return!0;b=b?b:window.e;var c=a.getTarget(b);if(c&&/INPUT|TEXTAREA|SELECT|OPTION/.test(c.nodeName)&&(!c.getAttribute("type")&&!c.type||!/submit|button|cancel/i.tp)||f(c).attr("contenteditable"))return!0; +if(a.hasfocus||a.hasmousefocus&&!y||a.ispage&&!y&&!D){c=b.keyCode;if(a.railslocked&&27!=c)return a.cancelEvent(b);var g=b.ctrlKey||!1,d=b.shiftKey||!1,e=!1;switch(c){case 38:case 63233:a.doScrollBy(72);e=!0;break;case 40:case 63235:a.doScrollBy(-72);e=!0;break;case 37:case 63232:a.railh&&(g?a.doScrollLeft(0):a.doScrollLeftBy(72),e=!0);break;case 39:case 63234:a.railh&&(g?a.doScrollLeft(a.page.maxw):a.doScrollLeftBy(-72),e=!0);break;case 33:case 63276:a.doScrollBy(a.view.h);e=!0;break;case 34:case 63277:a.doScrollBy(-a.view.h); +e=!0;break;case 36:case 63273:a.railh&&g?a.doScrollPos(0,0):a.doScrollTo(0);e=!0;break;case 35:case 63275:a.railh&&g?a.doScrollPos(a.page.maxw,a.page.maxh):a.doScrollTo(a.page.maxh);e=!0;break;case 32:a.opt.spacebarenabled&&(d?a.doScrollBy(a.view.h):a.doScrollBy(-a.view.h),e=!0);break;case 27:a.zoomactive&&(a.doZoom(),e=!0)}if(e)return a.cancelEvent(b)}};a.opt.enablekeyboard&&a.bind(document,e.isopera&&!e.isopera12?"keypress":"keydown",a.onkeypress);a.bind(document,"keydown",function(b){b.ctrlKey&& +(a.wheelprevented=!0)});a.bind(document,"keyup",function(b){b.ctrlKey||(a.wheelprevented=!1)});a.bind(window,"blur",function(b){a.wheelprevented=!1});a.bind(window,"resize",a.lazyResize);a.bind(window,"orientationchange",a.lazyResize);a.bind(window,"load",a.lazyResize);if(e.ischrome&&!a.ispage&&!a.haswrapper){var r=a.win.attr("style"),c=parseFloat(a.win.css("width"))+1;a.win.css("width",c);a.synched("chromefix",function(){a.win.attr("style",r)})}a.onAttributeChange=function(b){a.lazyResize(a.isieold? +250:30)};!1!==v&&(a.observerbody=new v(function(b){b.forEach(function(b){if("attributes"==b.type)return f("body").hasClass("modal-open")?a.hide():a.show()});if(document.body.scrollHeight!=a.page.maxh)return a.lazyResize(30)}),a.observerbody.observe(document.body,{childList:!0,subtree:!0,characterData:!1,attributes:!0,attributeFilter:["class"]}));a.ispage||a.haswrapper||(!1!==v?(a.observer=new v(function(b){b.forEach(a.onAttributeChange)}),a.observer.observe(a.win[0],{childList:!0,characterData:!1, +attributes:!0,subtree:!1}),a.observerremover=new v(function(b){b.forEach(function(b){if(0<b.removedNodes.length)for(var c in b.removedNodes)if(a&&b.removedNodes[c]==a.win[0])return a.remove()})}),a.observerremover.observe(a.win[0].parentNode,{childList:!0,characterData:!1,attributes:!1,subtree:!1})):(a.bind(a.win,e.isie&&!e.isie9?"propertychange":"DOMAttrModified",a.onAttributeChange),e.isie9&&a.win[0].attachEvent("onpropertychange",a.onAttributeChange),a.bind(a.win,"DOMNodeRemoved",function(b){b.target== +a.win[0]&&a.remove()})));!a.ispage&&a.opt.boxzoom&&a.bind(window,"resize",a.resizeZoom);a.istextarea&&a.bind(a.win,"mouseup",a.lazyResize);a.lazyResize(30)}if("IFRAME"==this.doc[0].nodeName){var M=function(){a.iframexd=!1;var b;try{b="contentDocument"in this?this.contentDocument:this.contentWindow.document}catch(c){a.iframexd=!0,b=!1}if(a.iframexd)return"console"in window&&console.log("NiceScroll error: policy restriced iframe"),!0;a.forcescreen=!0;a.isiframe&&(a.iframe={doc:f(b),html:a.doc.contents().find("html")[0], +body:a.doc.contents().find("body")[0]},a.getContentSize=function(){return{w:Math.max(a.iframe.html.scrollWidth,a.iframe.body.scrollWidth),h:Math.max(a.iframe.html.scrollHeight,a.iframe.body.scrollHeight)}},a.docscroll=f(a.iframe.body));if(!e.isios&&a.opt.iframeautoresize&&!a.isiframe){a.win.scrollTop(0);a.doc.height("");var g=Math.max(b.getElementsByTagName("html")[0].scrollHeight,b.body.scrollHeight);a.doc.height(g)}a.lazyResize(30);e.isie7&&a.css(f(a.iframe.html),{"overflow-y":"hidden"});a.css(f(a.iframe.body), +{"overflow-y":"hidden"});e.isios&&a.haswrapper&&a.css(f(b.body),{"-webkit-transform":"translate3d(0,0,0)"});"contentWindow"in this?a.bind(this.contentWindow,"scroll",a.onscroll):a.bind(b,"scroll",a.onscroll);a.opt.enablemousewheel&&a.bind(b,"mousewheel",a.onmousewheel);a.opt.enablekeyboard&&a.bind(b,e.isopera?"keypress":"keydown",a.onkeypress);if(e.cantouch||a.opt.touchbehavior)a.bind(b,"mousedown",a.ontouchstart),a.bind(b,"mousemove",function(b){return a.ontouchmove(b,!0)}),a.opt.grabcursorenabled&& +e.cursorgrabvalue&&a.css(f(b.body),{cursor:e.cursorgrabvalue});a.bind(b,"mouseup",a.ontouchend);a.zoom&&(a.opt.dblclickzoom&&a.bind(b,"dblclick",a.doZoom),a.ongesturezoom&&a.bind(b,"gestureend",a.ongesturezoom))};this.doc[0].readyState&&"complete"==this.doc[0].readyState&&setTimeout(function(){M.call(a.doc[0],!1)},500);a.bind(this.doc,"load",M)}};this.showCursor=function(b,c){a.cursortimeout&&(clearTimeout(a.cursortimeout),a.cursortimeout=0);if(a.rail){a.autohidedom&&(a.autohidedom.stop().css({opacity:a.opt.cursoropacitymax}), +a.cursoractive=!0);a.rail.drag&&1==a.rail.drag.pt||("undefined"!=typeof b&&!1!==b&&(a.scroll.y=Math.round(1*b/a.scrollratio.y)),"undefined"!=typeof c&&(a.scroll.x=Math.round(1*c/a.scrollratio.x)));a.cursor.css({height:a.cursorheight,top:a.scroll.y});if(a.cursorh){var d=a.hasreversehr?a.scrollvaluemaxw-a.scroll.x:a.scroll.x;!a.rail.align&&a.rail.visibility?a.cursorh.css({width:a.cursorwidth,left:d+a.rail.width}):a.cursorh.css({width:a.cursorwidth,left:d});a.cursoractive=!0}a.zoom&&a.zoom.stop().css({opacity:a.opt.cursoropacitymax})}}; +this.hideCursor=function(b){a.cursortimeout||!a.rail||!a.autohidedom||a.hasmousefocus&&"leave"==a.opt.autohidemode||(a.cursortimeout=setTimeout(function(){a.rail.active&&a.showonmouseevent||(a.autohidedom.stop().animate({opacity:a.opt.cursoropacitymin}),a.zoom&&a.zoom.stop().animate({opacity:a.opt.cursoropacitymin}),a.cursoractive=!1);a.cursortimeout=0},b||a.opt.hidecursordelay))};this.noticeCursor=function(b,c,d){a.showCursor(c,d);a.rail.active||a.hideCursor(b)};this.getContentSize=a.ispage?function(){return{w:Math.max(document.body.scrollWidth, +document.documentElement.scrollWidth),h:Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}}:a.haswrapper?function(){return{w:a.doc.outerWidth()+parseInt(a.win.css("paddingLeft"))+parseInt(a.win.css("paddingRight")),h:a.doc.outerHeight()+parseInt(a.win.css("paddingTop"))+parseInt(a.win.css("paddingBottom"))}}:function(){return{w:a.docscroll[0].scrollWidth,h:a.docscroll[0].scrollHeight}};this.onResize=function(b,c){if(!a||!a.win)return!1;if(!a.haswrapper&&!a.ispage){if("none"== +a.win.css("display"))return a.visibility&&a.hideRail().hideRailHr(),!1;a.hidden||a.visibility||a.showRail().showRailHr()}var d=a.page.maxh,e=a.page.maxw,f=a.view.h,h=a.view.w;a.view={w:a.ispage?a.win.width():parseInt(a.win[0].clientWidth),h:a.ispage?a.win.height():parseInt(a.win[0].clientHeight)};a.page=c?c:a.getContentSize();a.page.maxh=Math.max(0,a.page.h-a.view.h);a.page.maxw=Math.max(0,a.page.w-a.view.w);if(a.page.maxh==d&&a.page.maxw==e&&a.view.w==h&&a.view.h==f){if(a.ispage)return a;d=a.win.offset(); +if(a.lastposition&&(e=a.lastposition,e.top==d.top&&e.left==d.left))return a;a.lastposition=d}0==a.page.maxh?(a.hideRail(),a.scrollvaluemax=0,a.scroll.y=0,a.scrollratio.y=0,a.cursorheight=0,a.setScrollTop(0),a.rail.scrollable=!1):(a.page.maxh-=a.opt.railpadding.top+a.opt.railpadding.bottom,a.rail.scrollable=!0);0==a.page.maxw?(a.hideRailHr(),a.scrollvaluemaxw=0,a.scroll.x=0,a.scrollratio.x=0,a.cursorwidth=0,a.setScrollLeft(0),a.railh.scrollable=!1):(a.page.maxw-=a.opt.railpadding.left+a.opt.railpadding.right, +a.railh.scrollable=!0);a.railslocked=a.locked||0==a.page.maxh&&0==a.page.maxw;if(a.railslocked)return a.ispage||a.updateScrollBar(a.view),!1;a.hidden||a.visibility?a.hidden||a.railh.visibility||a.showRailHr():a.showRail().showRailHr();a.istextarea&&a.win.css("resize")&&"none"!=a.win.css("resize")&&(a.view.h-=20);a.cursorheight=Math.min(a.view.h,Math.round(a.view.h/a.page.h*a.view.h));a.cursorheight=a.opt.cursorfixedheight?a.opt.cursorfixedheight:Math.max(a.opt.cursorminheight,a.cursorheight);a.cursorwidth= +Math.min(a.view.w,Math.round(a.view.w/a.page.w*a.view.w));a.cursorwidth=a.opt.cursorfixedheight?a.opt.cursorfixedheight:Math.max(a.opt.cursorminheight,a.cursorwidth);a.scrollvaluemax=a.view.h-a.cursorheight-a.cursor.hborder-(a.opt.railpadding.top+a.opt.railpadding.bottom);a.railh&&(a.railh.width=0<a.page.maxh?a.view.w-a.rail.width:a.view.w,a.scrollvaluemaxw=a.railh.width-a.cursorwidth-a.cursorh.wborder-(a.opt.railpadding.left+a.opt.railpadding.right));a.ispage||a.updateScrollBar(a.view);a.scrollratio= +{x:a.page.maxw/a.scrollvaluemaxw,y:a.page.maxh/a.scrollvaluemax};a.getScrollTop()>a.page.maxh?a.doScrollTop(a.page.maxh):(a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y)),a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)),a.cursoractive&&a.noticeCursor());a.scroll.y&&0==a.getScrollTop()&&a.doScrollTo(Math.floor(a.scroll.y*a.scrollratio.y));return a};this.resize=a.onResize;this.lazyResize=function(b){b=isNaN(b)?30:b;a.debounced("resize",a.resize,b);return a};this.jqbind=function(b, +c,d){a.events.push({e:b,n:c,f:d,q:!0});f(b).bind(c,d)};this.bind=function(b,c,d,f){var h="jquery"in b?b[0]:b;"mousewheel"==c?window.addEventListener||"onwheel"in document?a._bind(h,"wheel",d,f||!1):(b="undefined"!=typeof document.onmousewheel?"mousewheel":"DOMMouseScroll",n(h,b,d,f||!1),"DOMMouseScroll"==b&&n(h,"MozMousePixelScroll",d,f||!1)):h.addEventListener?(e.cantouch&&/mouseup|mousedown|mousemove/.test(c)&&a._bind(h,"mousedown"==c?"touchstart":"mouseup"==c?"touchend":"touchmove",function(a){if(a.touches){if(2> +a.touches.length){var b=a.touches.length?a.touches[0]:a;b.original=a;d.call(this,b)}}else a.changedTouches&&(b=a.changedTouches[0],b.original=a,d.call(this,b))},f||!1),a._bind(h,c,d,f||!1),e.cantouch&&"mouseup"==c&&a._bind(h,"touchcancel",d,f||!1)):a._bind(h,c,function(b){(b=b||window.event||!1)&&b.srcElement&&(b.target=b.srcElement);"pageY"in b||(b.pageX=b.clientX+document.documentElement.scrollLeft,b.pageY=b.clientY+document.documentElement.scrollTop);return!1===d.call(h,b)||!1===f?a.cancelEvent(b): +!0})};e.haseventlistener?(this._bind=function(b,c,d,e){a.events.push({e:b,n:c,f:d,b:e,q:!1});b.addEventListener(c,d,e||!1)},this.cancelEvent=function(a){if(!a)return!1;a=a.original?a.original:a;a.preventDefault();a.stopPropagation();a.preventManipulation&&a.preventManipulation();return!1},this.stopPropagation=function(a){if(!a)return!1;a=a.original?a.original:a;a.stopPropagation();return!1},this._unbind=function(a,c,d,e){a.removeEventListener(c,d,e)}):(this._bind=function(b,c,d,e){a.events.push({e:b, +n:c,f:d,b:e,q:!1});b.attachEvent?b.attachEvent("on"+c,d):b["on"+c]=d},this.cancelEvent=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;a.cancel=!0;return a.returnValue=!1},this.stopPropagation=function(a){a=window.event||!1;if(!a)return!1;a.cancelBubble=!0;return!1},this._unbind=function(a,c,d,e){a.detachEvent?a.detachEvent("on"+c,d):a["on"+c]=!1});this.unbindAll=function(){for(var b=0;b<a.events.length;b++){var c=a.events[b];c.q?c.e.unbind(c.n,c.f):a._unbind(c.e,c.n,c.f,c.b)}};this.showRail= +function(){0==a.page.maxh||!a.ispage&&"none"==a.win.css("display")||(a.visibility=!0,a.rail.visibility=!0,a.rail.css("display","block"));return a};this.showRailHr=function(){if(!a.railh)return a;0==a.page.maxw||!a.ispage&&"none"==a.win.css("display")||(a.railh.visibility=!0,a.railh.css("display","block"));return a};this.hideRail=function(){a.visibility=!1;a.rail.visibility=!1;a.rail.css("display","none");return a};this.hideRailHr=function(){if(!a.railh)return a;a.railh.visibility=!1;a.railh.css("display", +"none");return a};this.show=function(){a.hidden=!1;a.railslocked=!1;return a.showRail().showRailHr()};this.hide=function(){a.hidden=!0;a.railslocked=!0;return a.hideRail().hideRailHr()};this.toggle=function(){return a.hidden?a.show():a.hide()};this.remove=function(){a.stop();a.cursortimeout&&clearTimeout(a.cursortimeout);a.doZoomOut();a.unbindAll();e.isie9&&a.win[0].detachEvent("onpropertychange",a.onAttributeChange);!1!==a.observer&&a.observer.disconnect();!1!==a.observerremover&&a.observerremover.disconnect(); +!1!==a.observerbody&&a.observerbody.disconnect();a.events=null;a.cursor&&a.cursor.remove();a.cursorh&&a.cursorh.remove();a.rail&&a.rail.remove();a.railh&&a.railh.remove();a.zoom&&a.zoom.remove();for(var b=0;b<a.saved.css.length;b++){var c=a.saved.css[b];c[0].css(c[1],"undefined"==typeof c[2]?"":c[2])}a.saved=!1;a.me.data("__nicescroll","");var d=f.nicescroll;d.each(function(b){if(this&&this.id===a.id){delete d[b];for(var c=++b;c<d.length;c++,b++)d[b]=d[c];d.length--;d.length&&delete d[d.length]}}); +for(var h in a)a[h]=null,delete a[h];a=null};this.scrollstart=function(b){this.onscrollstart=b;return a};this.scrollend=function(b){this.onscrollend=b;return a};this.scrollcancel=function(b){this.onscrollcancel=b;return a};this.zoomin=function(b){this.onzoomin=b;return a};this.zoomout=function(b){this.onzoomout=b;return a};this.isScrollable=function(a){a=a.target?a.target:a;if("OPTION"==a.nodeName)return!0;for(;a&&1==a.nodeType&&!/^BODY|HTML/.test(a.nodeName);){var c=f(a),c=c.css("overflowY")||c.css("overflowX")|| +c.css("overflow")||"";if(/scroll|auto/.test(c))return a.clientHeight!=a.scrollHeight;a=a.parentNode?a.parentNode:!1}return!1};this.getViewport=function(a){for(a=a&&a.parentNode?a.parentNode:!1;a&&1==a.nodeType&&!/^BODY|HTML/.test(a.nodeName);){var c=f(a);if(/fixed|absolute/.test(c.css("position")))return c;var d=c.css("overflowY")||c.css("overflowX")||c.css("overflow")||"";if(/scroll|auto/.test(d)&&a.clientHeight!=a.scrollHeight||0<c.getNiceScroll().length)return c;a=a.parentNode?a.parentNode:!1}return!1}; +this.triggerScrollEnd=function(){if(a.onscrollend){var b=a.getScrollLeft(),c=a.getScrollTop();a.onscrollend.call(a,{type:"scrollend",current:{x:b,y:c},end:{x:b,y:c}})}};this.onmousewheel=function(b){if(!a.wheelprevented){if(a.railslocked)return a.debounced("checkunlock",a.resize,250),!0;if(a.rail.drag)return a.cancelEvent(b);"auto"==a.opt.oneaxismousemode&&0!=b.deltaX&&(a.opt.oneaxismousemode=!1);if(a.opt.oneaxismousemode&&0==b.deltaX&&!a.rail.scrollable)return a.railh&&a.railh.scrollable?a.onmousewheelhr(b): +!0;var c=+new Date,d=!1;a.opt.preservenativescrolling&&a.checkarea+600<c&&(a.nativescrollingarea=a.isScrollable(b),d=!0);a.checkarea=c;if(a.nativescrollingarea)return!0;if(b=p(b,!1,d))a.checkarea=0;return b}};this.onmousewheelhr=function(b){if(!a.wheelprevented){if(a.railslocked||!a.railh.scrollable)return!0;if(a.rail.drag)return a.cancelEvent(b);var c=+new Date,d=!1;a.opt.preservenativescrolling&&a.checkarea+600<c&&(a.nativescrollingarea=a.isScrollable(b),d=!0);a.checkarea=c;return a.nativescrollingarea? +!0:a.railslocked?a.cancelEvent(b):p(b,!0,d)}};this.stop=function(){a.cancelScroll();a.scrollmon&&a.scrollmon.stop();a.cursorfreezed=!1;a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y));a.noticeCursor();return a};this.getTransitionSpeed=function(b){var c=Math.round(10*a.opt.scrollspeed);b=Math.min(c,Math.round(b/20*a.opt.scrollspeed));return 20<b?b:0};a.opt.smoothscroll?a.ishwscroll&&e.hastransition&&a.opt.usetransition&&a.opt.smoothscroll?(this.prepareTransition=function(b,c){var d=c?20< +b?b:0:a.getTransitionSpeed(b),f=d?e.prefixstyle+"transform "+d+"ms ease-out":"";a.lasttransitionstyle&&a.lasttransitionstyle==f||(a.lasttransitionstyle=f,a.doc.css(e.transitionstyle,f));return d},this.doScrollLeft=function(b,c){var d=a.scrollrunning?a.newscrolly:a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,c){var d=a.scrollrunning?a.newscrollx:a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){var f=a.getScrollTop(),h=a.getScrollLeft();(0>(a.newscrolly- +f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();0==a.opt.bouncescroll&&(0>c?c=0:c>a.page.maxh&&(c=a.page.maxh),0>b?b=0:b>a.page.maxw&&(b=a.page.maxw));if(a.scrollrunning&&b==a.newscrollx&&c==a.newscrolly)return!1;a.newscrolly=c;a.newscrollx=b;a.newscrollspeed=d||!1;if(a.timer)return!1;a.timer=setTimeout(function(){var d=a.getScrollTop(),f=a.getScrollLeft(),h,k;h=b-f;k=c-d;h=Math.round(Math.sqrt(Math.pow(h,2)+Math.pow(k,2)));h=a.newscrollspeed&&1<a.newscrollspeed?a.newscrollspeed:a.getTransitionSpeed(h); +a.newscrollspeed&&1>=a.newscrollspeed&&(h*=a.newscrollspeed);a.prepareTransition(h,!0);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);0<h&&(!a.scrollrunning&&a.onscrollstart&&a.onscrollstart.call(a,{type:"scrollstart",current:{x:f,y:d},request:{x:b,y:c},end:{x:a.newscrollx,y:a.newscrolly},speed:h}),e.transitionend?a.scrollendtrapped||(a.scrollendtrapped=!0,a.bind(a.doc,e.transitionend,a.onScrollTransitionEnd,!1)):(a.scrollendtrapped&&clearTimeout(a.scrollendtrapped),a.scrollendtrapped= +setTimeout(a.onScrollTransitionEnd,h)),a.timerscroll={bz:new A(d,a.newscrolly,h,0,0,.58,1),bh:new A(f,a.newscrollx,h,0,0,.58,1)},a.cursorfreezed||(a.timerscroll.tm=setInterval(function(){a.showCursor(a.getScrollTop(),a.getScrollLeft())},60)));a.synched("doScroll-set",function(){a.timer=0;a.scrollendtrapped&&(a.scrollrunning=!0);a.setScrollTop(a.newscrolly);a.setScrollLeft(a.newscrollx);if(!a.scrollendtrapped)a.onScrollTransitionEnd()})},50)},this.cancelScroll=function(){if(!a.scrollendtrapped)return!0; +var b=a.getScrollTop(),c=a.getScrollLeft();a.scrollrunning=!1;e.transitionend||clearTimeout(e.transitionend);a.scrollendtrapped=!1;a._unbind(a.doc[0],e.transitionend,a.onScrollTransitionEnd);a.prepareTransition(0);a.setScrollTop(b);a.railh&&a.setScrollLeft(c);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);a.timerscroll=!1;a.cursorfreezed=!1;a.showCursor(b,c);return a},this.onScrollTransitionEnd=function(){a.scrollendtrapped&&a._unbind(a.doc[0],e.transitionend,a.onScrollTransitionEnd); +a.scrollendtrapped=!1;a.prepareTransition(0);a.timerscroll&&a.timerscroll.tm&&clearInterval(a.timerscroll.tm);a.timerscroll=!1;var b=a.getScrollTop(),c=a.getScrollLeft();a.setScrollTop(b);a.railh&&a.setScrollLeft(c);a.noticeCursor(!1,b,c);a.cursorfreezed=!1;0>b?b=0:b>a.page.maxh&&(b=a.page.maxh);0>c?c=0:c>a.page.maxw&&(c=a.page.maxw);if(b!=a.newscrolly||c!=a.newscrollx)return a.doScrollPos(c,b,a.opt.snapbackspeed);a.onscrollend&&a.scrollrunning&&a.triggerScrollEnd();a.scrollrunning=!1}):(this.doScrollLeft= +function(b,c){var d=a.scrollrunning?a.newscrolly:a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b,c){var d=a.scrollrunning?a.newscrollx:a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){function e(){if(a.cancelAnimationFrame)return!0;a.scrollrunning=!0;if(n=1-n)return a.timer=s(e)||1;var b=0,c,d,g=d=a.getScrollTop();if(a.dst.ay){g=a.bzscroll?a.dst.py+a.bzscroll.getNow()*a.dst.ay:a.newscrolly;c=g-d;if(0>c&&g<a.newscrolly||0<c&&g>a.newscrolly)g=a.newscrolly; +a.setScrollTop(g);g==a.newscrolly&&(b=1)}else b=1;d=c=a.getScrollLeft();if(a.dst.ax){d=a.bzscroll?a.dst.px+a.bzscroll.getNow()*a.dst.ax:a.newscrollx;c=d-c;if(0>c&&d<a.newscrollx||0<c&&d>a.newscrollx)d=a.newscrollx;a.setScrollLeft(d);d==a.newscrollx&&(b+=1)}else b+=1;2==b?(a.timer=0,a.cursorfreezed=!1,a.bzscroll=!1,a.scrollrunning=!1,0>g?g=0:g>a.page.maxh&&(g=a.page.maxh),0>d?d=0:d>a.page.maxw&&(d=a.page.maxw),d!=a.newscrollx||g!=a.newscrolly?a.doScrollPos(d,g):a.onscrollend&&a.triggerScrollEnd()): +a.timer=s(e)||1}c="undefined"==typeof c||!1===c?a.getScrollTop(!0):c;if(a.timer&&a.newscrolly==c&&a.newscrollx==b)return!0;a.timer&&t(a.timer);a.timer=0;var f=a.getScrollTop(),h=a.getScrollLeft();(0>(a.newscrolly-f)*(c-f)||0>(a.newscrollx-h)*(b-h))&&a.cancelScroll();a.newscrolly=c;a.newscrollx=b;a.bouncescroll&&a.rail.visibility||(0>a.newscrolly?a.newscrolly=0:a.newscrolly>a.page.maxh&&(a.newscrolly=a.page.maxh));a.bouncescroll&&a.railh.visibility||(0>a.newscrollx?a.newscrollx=0:a.newscrollx>a.page.maxw&& +(a.newscrollx=a.page.maxw));a.dst={};a.dst.x=b-h;a.dst.y=c-f;a.dst.px=h;a.dst.py=f;var k=Math.round(Math.sqrt(Math.pow(a.dst.x,2)+Math.pow(a.dst.y,2)));a.dst.ax=a.dst.x/k;a.dst.ay=a.dst.y/k;var l=0,m=k;0==a.dst.x?(l=f,m=c,a.dst.ay=1,a.dst.py=0):0==a.dst.y&&(l=h,m=b,a.dst.ax=1,a.dst.px=0);k=a.getTransitionSpeed(k);d&&1>=d&&(k*=d);a.bzscroll=0<k?a.bzscroll?a.bzscroll.update(m,k):new A(l,m,k,0,1,0,1):!1;if(!a.timer){(f==a.page.maxh&&c>=a.page.maxh||h==a.page.maxw&&b>=a.page.maxw)&&a.checkContentSize(); +var n=1;a.cancelAnimationFrame=!1;a.timer=1;a.onscrollstart&&!a.scrollrunning&&a.onscrollstart.call(a,{type:"scrollstart",current:{x:h,y:f},request:{x:b,y:c},end:{x:a.newscrollx,y:a.newscrolly},speed:k});e();(f==a.page.maxh&&c>=f||h==a.page.maxw&&b>=h)&&a.checkContentSize();a.noticeCursor()}},this.cancelScroll=function(){a.timer&&t(a.timer);a.timer=0;a.bzscroll=!1;a.scrollrunning=!1;return a}):(this.doScrollLeft=function(b,c){var d=a.getScrollTop();a.doScrollPos(b,d,c)},this.doScrollTop=function(b, +c){var d=a.getScrollLeft();a.doScrollPos(d,b,c)},this.doScrollPos=function(b,c,d){var e=b>a.page.maxw?a.page.maxw:b;0>e&&(e=0);var f=c>a.page.maxh?a.page.maxh:c;0>f&&(f=0);a.synched("scroll",function(){a.setScrollTop(f);a.setScrollLeft(e)})},this.cancelScroll=function(){});this.doScrollBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.y-b)*a.scrollratio.y):(a.timer?a.newscrolly:a.getScrollTop(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.h/2);d<-e?d=-e:d>a.page.maxh+e&&(d=a.page.maxh+e)}a.cursorfreezed= +!1;e=a.getScrollTop(!0);if(0>d&&0>=e)return a.noticeCursor();if(d>a.page.maxh&&e>=a.page.maxh)return a.checkContentSize(),a.noticeCursor();a.doScrollTop(d)};this.doScrollLeftBy=function(b,c){var d=0,d=c?Math.floor((a.scroll.x-b)*a.scrollratio.x):(a.timer?a.newscrollx:a.getScrollLeft(!0))-b;if(a.bouncescroll){var e=Math.round(a.view.w/2);d<-e?d=-e:d>a.page.maxw+e&&(d=a.page.maxw+e)}a.cursorfreezed=!1;e=a.getScrollLeft(!0);if(0>d&&0>=e||d>a.page.maxw&&e>=a.page.maxw)return a.noticeCursor();a.doScrollLeft(d)}; +this.doScrollTo=function(b,c){c&&Math.round(b*a.scrollratio.y);a.cursorfreezed=!1;a.doScrollTop(b)};this.checkContentSize=function(){var b=a.getContentSize();b.h==a.page.h&&b.w==a.page.w||a.resize(!1,b)};a.onscroll=function(b){a.rail.drag||a.cursorfreezed||a.synched("scroll",function(){a.scroll.y=Math.round(a.getScrollTop()*(1/a.scrollratio.y));a.railh&&(a.scroll.x=Math.round(a.getScrollLeft()*(1/a.scrollratio.x)));a.noticeCursor()})};a.bind(a.docscroll,"scroll",a.onscroll);this.doZoomIn=function(b){if(!a.zoomactive){a.zoomactive= +!0;a.zoomrestore={style:{}};var c="position top left zIndex backgroundColor marginTop marginBottom marginLeft marginRight".split(" "),d=a.win[0].style,h;for(h in c){var k=c[h];a.zoomrestore.style[k]="undefined"!=typeof d[k]?d[k]:""}a.zoomrestore.style.width=a.win.css("width");a.zoomrestore.style.height=a.win.css("height");a.zoomrestore.padding={w:a.win.outerWidth()-a.win.width(),h:a.win.outerHeight()-a.win.height()};e.isios4&&(a.zoomrestore.scrollTop=f(window).scrollTop(),f(window).scrollTop(0)); +a.win.css({position:e.isios4?"absolute":"fixed",top:0,left:0,"z-index":x+100,margin:"0px"});c=a.win.css("backgroundColor");(""==c||/transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(c))&&a.win.css("backgroundColor","#fff");a.rail.css({"z-index":x+101});a.zoom.css({"z-index":x+102});a.zoom.css("backgroundPosition","0px -18px");a.resizeZoom();a.onzoomin&&a.onzoomin.call(a);return a.cancelEvent(b)}};this.doZoomOut=function(b){if(a.zoomactive)return a.zoomactive=!1,a.win.css("margin",""),a.win.css(a.zoomrestore.style), +e.isios4&&f(window).scrollTop(a.zoomrestore.scrollTop),a.rail.css({"z-index":a.zindex}),a.zoom.css({"z-index":a.zindex}),a.zoomrestore=!1,a.zoom.css("backgroundPosition","0px 0px"),a.onResize(),a.onzoomout&&a.onzoomout.call(a),a.cancelEvent(b)};this.doZoom=function(b){return a.zoomactive?a.doZoomOut(b):a.doZoomIn(b)};this.resizeZoom=function(){if(a.zoomactive){var b=a.getScrollTop();a.win.css({width:f(window).width()-a.zoomrestore.padding.w+"px",height:f(window).height()-a.zoomrestore.padding.h+"px"}); +a.onResize();a.setScrollTop(Math.min(a.page.maxh,b))}};this.init();f.nicescroll.push(this)},L=function(f){var c=this;this.nc=f;this.steptime=this.lasttime=this.speedy=this.speedx=this.lasty=this.lastx=0;this.snapy=this.snapx=!1;this.demuly=this.demulx=0;this.lastscrolly=this.lastscrollx=-1;this.timer=this.chky=this.chkx=0;this.time=function(){return+new Date};this.reset=function(f,k){c.stop();var d=c.time();c.steptime=0;c.lasttime=d;c.speedx=0;c.speedy=0;c.lastx=f;c.lasty=k;c.lastscrollx=-1;c.lastscrolly= +-1};this.update=function(f,k){var d=c.time();c.steptime=d-c.lasttime;c.lasttime=d;var d=k-c.lasty,n=f-c.lastx,p=c.nc.getScrollTop(),a=c.nc.getScrollLeft(),p=p+d,a=a+n;c.snapx=0>a||a>c.nc.page.maxw;c.snapy=0>p||p>c.nc.page.maxh;c.speedx=n;c.speedy=d;c.lastx=f;c.lasty=k};this.stop=function(){c.nc.unsynched("domomentum2d");c.timer&&clearTimeout(c.timer);c.timer=0;c.lastscrollx=-1;c.lastscrolly=-1};this.doSnapy=function(f,k){var d=!1;0>k?(k=0,d=!0):k>c.nc.page.maxh&&(k=c.nc.page.maxh,d=!0);0>f?(f=0,d= +!0):f>c.nc.page.maxw&&(f=c.nc.page.maxw,d=!0);d?c.nc.doScrollPos(f,k,c.nc.opt.snapbackspeed):c.nc.triggerScrollEnd()};this.doMomentum=function(f){var k=c.time(),d=f?k+f:c.lasttime;f=c.nc.getScrollLeft();var n=c.nc.getScrollTop(),p=c.nc.page.maxh,a=c.nc.page.maxw;c.speedx=0<a?Math.min(60,c.speedx):0;c.speedy=0<p?Math.min(60,c.speedy):0;d=d&&60>=k-d;if(0>n||n>p||0>f||f>a)d=!1;f=c.speedx&&d?c.speedx:!1;if(c.speedy&&d&&c.speedy||f){var s=Math.max(16,c.steptime);50<s&&(f=s/50,c.speedx*=f,c.speedy*=f,s= +50);c.demulxy=0;c.lastscrollx=c.nc.getScrollLeft();c.chkx=c.lastscrollx;c.lastscrolly=c.nc.getScrollTop();c.chky=c.lastscrolly;var e=c.lastscrollx,r=c.lastscrolly,t=function(){var d=600<c.time()-k?.04:.02;c.speedx&&(e=Math.floor(c.lastscrollx-c.speedx*(1-c.demulxy)),c.lastscrollx=e,0>e||e>a)&&(d=.1);c.speedy&&(r=Math.floor(c.lastscrolly-c.speedy*(1-c.demulxy)),c.lastscrolly=r,0>r||r>p)&&(d=.1);c.demulxy=Math.min(1,c.demulxy+d);c.nc.synched("domomentum2d",function(){c.speedx&&(c.nc.getScrollLeft()!= +c.chkx&&c.stop(),c.chkx=e,c.nc.setScrollLeft(e));c.speedy&&(c.nc.getScrollTop()!=c.chky&&c.stop(),c.chky=r,c.nc.setScrollTop(r));c.timer||(c.nc.hideCursor(),c.doSnapy(e,r))});1>c.demulxy?c.timer=setTimeout(t,s):(c.stop(),c.nc.hideCursor(),c.doSnapy(e,r))};t()}else c.doSnapy(c.nc.getScrollLeft(),c.nc.getScrollTop())}},w=f.fn.scrollTop;f.cssHooks.pageYOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollTop():w.call(k)},set:function(k,c){var h=f.data(k,"__nicescroll")|| +!1;h&&h.ishwscroll?h.setScrollTop(parseInt(c)):w.call(k,c);return this}};f.fn.scrollTop=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollTop():w.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollTop(parseInt(k)):w.call(f(this),k)})};var B=f.fn.scrollLeft;f.cssHooks.pageXOffset={get:function(k,c,h){return(c=f.data(k,"__nicescroll")||!1)&&c.ishwscroll?c.getScrollLeft():B.call(k)}, +set:function(k,c){var h=f.data(k,"__nicescroll")||!1;h&&h.ishwscroll?h.setScrollLeft(parseInt(c)):B.call(k,c);return this}};f.fn.scrollLeft=function(k){if("undefined"==typeof k){var c=this[0]?f.data(this[0],"__nicescroll")||!1:!1;return c&&c.ishwscroll?c.getScrollLeft():B.call(this)}return this.each(function(){var c=f.data(this,"__nicescroll")||!1;c&&c.ishwscroll?c.setScrollLeft(parseInt(k)):B.call(f(this),k)})};var C=function(k){var c=this;this.length=0;this.name="nicescrollarray";this.each=function(d){for(var f= +0,h=0;f<c.length;f++)d.call(c[f],h++);return c};this.push=function(d){c[c.length]=d;c.length++};this.eq=function(d){return c[d]};if(k)for(var h=0;h<k.length;h++){var m=f.data(k[h],"__nicescroll")||!1;m&&(this[this.length]=m,this.length++)}return this};(function(f,c,h){for(var m=0;m<c.length;m++)h(f,c[m])})(C.prototype,"show hide toggle onResize resize remove stop doScrollPos".split(" "),function(f,c){f[c]=function(){var f=arguments;return this.each(function(){this[c].apply(this,f)})}});f.fn.getNiceScroll= +function(k){return"undefined"==typeof k?new C(this):this[k]&&f.data(this[k],"__nicescroll")||!1};f.extend(f.expr[":"],{nicescroll:function(k){return f.data(k,"__nicescroll")?!0:!1}});f.fn.niceScroll=function(k,c){"undefined"!=typeof c||"object"!=typeof k||"jquery"in k||(c=k,k=!1);c=f.extend({},c);var h=new C;"undefined"==typeof c&&(c={});k&&(c.doc=f(k),c.win=f(this));var m=!("doc"in c);m||"win"in c||(c.win=f(this));this.each(function(){var d=f(this).data("__nicescroll")||!1;d||(c.doc=m?f(this):c.doc, +d=new R(c,f(this)),f(this).data("__nicescroll",d));h.push(d)});return 1==h.length?h[0]:h};window.NiceScroll={getjQuery:function(){return f}};f.nicescroll||(f.nicescroll=new C,f.nicescroll.options=I)}); |
