diff options
497 files changed, 4374 insertions, 1737 deletions
diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..453747e14e1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +/public/ +/tmp/ +/vendor/ + diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000000..16eb18ecba2 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "extends": "airbnb", + "globals": { + "$": false, + "_": false, + "beforeEach": false, + "d3": false, + "define": false, + "describe": false, + "document": false, + "expect": false, + "fixture": false, + "gl": false, + "it": false, + "jQuery": false, + "Mousetrap": false, + "spyOn": false, + "spyOnEvent": false, + "Turbolinks": false, + "window": false + } +} + diff --git a/.gitignore b/.gitignore index 9166512606d..6a1002621f4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ /doc/code/* /dump.rdb /log/*.log* +/node_modules/ /nohup.out /public/assets/ /public/uploads.* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 474bf6765f4..d04069df885 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -215,6 +215,7 @@ rake ee_compat_check: only: - branches except: + - master - tags allow_failure: yes @@ -248,7 +249,7 @@ teaspoon: - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - apt-get install --assume-yes nodejs - npm install --global istanbul - - teaspoon + - rake teaspoon artifacts: name: coverage-javascript expire_in: 31d @@ -310,16 +311,29 @@ coverage: - coverage/index.html - coverage/assets/ +lint-javascript: + stage: test + image: "node:latest" + before_script: + - npm install + script: + - npm run eslint + # Trigger docs build +# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process trigger_docs: stage: post-test - before_script: [] + image: "alpine" + before_script: + - apk update && apk add curl + variables: + GIT_STRATEGY: none cache: {} artifacts: {} script: - - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds" + - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds" only: - - master + - master@gitlab-org/gitlab-ce # Notify slack in the end diff --git a/.scss-lint.yml b/.scss-lint.yml index 5c8e5ac0758..aae8d9b6dbe 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -143,7 +143,7 @@ linters: # with two colons. Pseudo-classes, like :hover and :first-child, should # be declared with one colon. PseudoElement: - enabled: false + enabled: true # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: diff --git a/CHANGELOG.md b/CHANGELOG.md index 11747ce363d..188e6ca0332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,170 +23,243 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix documents and comments on Build API `scope` - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) +- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 +- Adds user project membership expired event to clarify why user was removed (Callum Dryden) +- Trim leading and trailing whitespace on project_path (Linus Thiel) +- Prevent award emoji via notes for issues/MRs authored by user (barthc) +- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) +- Fix Markdown styling inside reference links (Jan Zdráhal) +- Fix extra space on Build sidebar on Firefox !7060 +- Fix mobile layout issues in admin user overview page !7087 +- Fix HipChat notifications rendering (airatshigapov, eisnerd) +- Refactor Jira service to use jira-ruby gem +- Improved todos empty state +- Add hover to trash icon in notes !7008 (blackst0ne) +- Only show one error message for an invalid email !5905 (lycoperdon) +- Fix sidekiq stats in admin area (blackst0ne) +- Created cycle analytics bundle JavaScript file +- API: Fix booleans not recognized as such when using the `to_boolean` helper +- Removed delete branch tooltip !6954 +- Stop unauthorized users dragging on milestone page (blackst0ne) +- Restore issue boards welcome message when a project is created !6899 +- Do not show tooltip for active element !7105 (winniehell) +- Escape ref and path for relative links !6050 (winniehell) +- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) +- Fix filtering of milestones with quotes in title (airatshigapov) +- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) +- Update mail_room and enable sentinel support to Reply By Email (!7101) +- Add task completion status in Issues and Merge Requests tabs: "X of Y tasks completed" (!6527, @gmesalazar) +- Simpler arguments passed to named_route on toggle_award_url helper method +- Fix typo in framework css class. !7086 (Daniel Voogsgerd) +- New issue board list dropdown stays open after adding a new list +- Fix: Backup restore doesn't clear cache +- Optimize Event queries by removing default order +- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) +- Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld) +- Replace jquery.cookie plugin with js.cookie !7085 +- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method +- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens +- Show full status link on MR & commit pipelines +- Fix documents and comments on Build API `scope` +- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) +- Shortened merge request modal to let clipboard button not overlap +- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo) +- Improve search query parameter naming in /admin/users !7115 (YarNayar) +- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar) + +## 8.13.3 + +- Fix relative links in Markdown wiki when displayed in "Project" tab !7218 +- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project +- Fix project features default values + +## 8.13.2 (2016-10-31) + +- Fix encoding issues on pipeline commits. !6832 +- Use Hash rocket syntax to fix cycle analytics under Ruby 2.1. !6977 +- Modify GitHub importer to be retryable. !7003 +- Fix refs dropdown selection with special characters. !7061 +- Fix horizontal padding for highlight blocks. !7062 +- Pass user instance to `Labels::FindOrCreateService` or `skip_authorization: true`. !7093 +- Fix builds dropdown overlapping bug. !7124 +- Fix applying labels for GitHub-imported MRs. !7139 +- Fix importing MR comments from GitHub. !7139 +- Fix project member access for group links. !7144 +- API: Fix booleans not recognized as such when using the `to_boolean` helper. !7149 +- Fix and improve `Sortable.highest_label_priority`. !7165 +- Fixed sticky merge request tabs when sidebar is pinned. !7167 +- Only remove right connector of first build of last stage. !7179 + ## 8.13.1 (2016-10-25) - - Fix branch protection API. !6215 - - Fix hidden pipeline graph on commit and MR page. !6895 - - Fix Cycle analytics not showing correct data when filtering by date. !6906 - - Ensure custom provider tab labels don't break layout. !6993 - - Fix issue boards user link when in subdirectory. !7018 - - Refactor and add new environment functionality to CI yaml reference. !7026 - - Fix typo in project settings that prevents users from enabling container registry. !7037 - - Fix events order in `users/:id/events` endpoint. !7039 - - Remove extra line for empty issue description. !7045 - - Don't append issue/MR templates to any existing text. !7050 - - Fix error in generating labels. !7055 - - Stop clearing the database cache on `rake cache:clear`. !7056 - - Only show register tab if signup enabled. !7058 - - Expire and build repository cache after project import. !7064 - - Fix bug where labels would be assigned to issues that were moved. !7065 - - Fix reply-by-email not working due to queue name mismatch. !7068 - - Fix 404 for group pages when GitLab setup uses relative url. !7071 - - Fix `User#to_reference`. !7088 - - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 - - Fix unauthorized users dragging on issue boards. !7096 - - Only schedule `ProjectCacheWorker` jobs when needed. !7099 + +- Fix branch protection API. !6215 +- Fix hidden pipeline graph on commit and MR page. !6895 +- Fix Cycle analytics not showing correct data when filtering by date. !6906 +- Ensure custom provider tab labels don't break layout. !6993 +- Fix issue boards user link when in subdirectory. !7018 +- Refactor and add new environment functionality to CI yaml reference. !7026 +- Fix typo in project settings that prevents users from enabling container registry. !7037 +- Fix events order in `users/:id/events` endpoint. !7039 +- Remove extra line for empty issue description. !7045 +- Don't append issue/MR templates to any existing text. !7050 +- Fix error in generating labels. !7055 +- Stop clearing the database cache on `rake cache:clear`. !7056 +- Only show register tab if signup enabled. !7058 +- Fix lightweight tags not processed correctly by GitTagPushService +- Expire and build repository cache after project import. !7064 +- Fix bug where labels would be assigned to issues that were moved. !7065 +- Fix reply-by-email not working due to queue name mismatch. !7068 +- Fix 404 for group pages when GitLab setup uses relative url. !7071 +- Fix `User#to_reference`. !7088 +- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 +- Fix unauthorized users dragging on issue boards. !7096 +- Only schedule `ProjectCacheWorker` jobs when needed. !7099 ## 8.13.0 (2016-10-22) - - Fix save button on project pipeline settings page. (!6955) - - All Sidekiq workers now use their own queue - - Avoid race condition when asynchronously removing expired artifacts. (!6881) - - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - - Respond with 404 Not Found for non-existent tags (Linus Thiel) - - Truncate long labels with ellipsis in labels page - - Improve tabbing usability for sign in page (ClemMakesApps) - - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint - - Adding members no longer silently fails when there is extra whitespace - - Update runner version only when updating contacted_at - - Add link from system note to compare with previous version - - Use gitlab-shell v3.6.6 - - Ignore references to internal issues when using external issues tracker - - Ability to resolve merge request conflicts with editor !6374 - - Add `/projects/visible` API endpoint (Ben Boeckel) - - Fix centering of custom header logos (Ashley Dumaine) - - Keep around commits only pipeline creation as pipeline data doesn't change over time - - Update duration at the end of pipeline - - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - - Add group level labels. (!6425) - - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - - Cancelled pipelines could be retried. !6927 - - Updating verbiage on git basics to be more intuitive - - Fix project_feature record not generated on project creation - - Clarify documentation for Runners API (Gennady Trafimenkov) - - The instrumentation for Banzai::Renderer has been restored - - Change user & group landing page routing from /u/:username to /:username - - Added documentation for .gitattributes files - - Move Pipeline Metrics to separate worker - - AbstractReferenceFilter caches project_refs on RequestStore when active - - Replaced the check sign to arrow in the show build view. !6501 - - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - - ProjectCacheWorker updates caches at most once per 15 minutes per project - - Fix Error 500 when viewing old merge requests with bad diff data - - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - - Fix viewing merged MRs when the source project has been removed !6991 - - Speed-up group milestones show page - - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService - - Fix discussion thread from emails for merge requests. !7010 - - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - - Add tag shortcut from the Commit page. !6543 - - Keep refs for each deployment - - Close open tooltips on page navigation (Linus Thiel) - - Allow browsing branches that end with '.atom' - - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) - - Add more tests for calendar contribution (ClemMakesApps) - - Update Gitlab Shell to fix some problems with moving projects between storages - - Cache rendered markdown in the database, rather than Redis - - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - - Simplify Mentionable concern instance methods - - API: Ability to retrieve version information (Robert Schilling) - - Fix permission for setting an issue's due date - - API: Multi-file commit !6096 (mahcsig) - - Unicode emoji are now converted to images - - Revert "Label list shows all issues (opened or closed) with that label" - - Expose expires_at field when sharing project on API - - Fix VueJS template tags being rendered in code comments - - Added copy file path button to merge request diff files - - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - - Add Issue Board API support (andrebsguedes) - - Allow the Koding integration to be configured through the API - - Add new issue button to each list on Issues Board - - Execute specific named route method from toggle_award_url helper method - - Added soft wrap button to repository file/blob editor - - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - - Show the time ago a merge request was deployed to an environment - - Add RTL support to markdown renderer (Ebrahim Byagowi) - - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - - Fix todos page mobile viewport layout (ClemMakesApps) - - Make issues search less finicky - - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - - Remove redundant mixins (ClemMakesApps) - - Added 'Download' button to the Snippets page (Justin DiPierro) - - Add visibility level to project repository - - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - - Fix showing commits from source project for merge request !6658 - - Fix that manual jobs would no longer block jobs in the next stage. !6604 - - Add configurable email subject suffix (Fu Xu) - - Use defined colour for a language when available !6748 (nilsding) - - Added tooltip to fork count on project show page. (Justin DiPierro) - - Use a ConnectionPool for Rails.cache on Sidekiq servers - - Replace `alias_method_chain` with `Module#prepend` - - Enable GitLab Import/Export for non-admin users. - - Preserve label filters when sorting !6136 (Joseph Frazier) - - MergeRequest#new form load diff asynchronously - - Only update issuable labels if they have been changed - - Take filters in account in issuable counters. !6496 - - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - - Append issue template to existing description !6149 (Joseph Frazier) - - Trending projects now only show public projects and the list of projects is cached for a day - - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) - - Revoke button in Applications Settings underlines on hover. - - Use higher size on Gitlab::Redis connection pool on Sidekiq servers - - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - - Revert avoid touching file system on Build#artifacts? - - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created - - Add disabled delete button to protected branches (ClemMakesApps) - - Add broadcast messages and alerts below sub-nav - - Better empty state for Groups view - - API: New /users/:id/events endpoint - - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - - Replace bootstrap caret with fontawesome caret (ClemMakesApps) - - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - - Add organization field to user profile - - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being. - - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - - Fix deploy status responsiveness error !6633 - - Make searching for commits case insensitive - - Fix resolved discussion display in side-by-side diff view !6575 - - Optimize GitHub importing for speed and memory - - API: expose pipeline data in builds API (!6502, Guilherme Salazar) - - Notify the Merger about merge after successful build (Dimitris Karakasilis) - - Reduce queries needed to find users using their SSH keys when pushing commits - - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - - Fix broken repository 500 errors in project list - - Fix the diff in the merge request view when converting a symlink to a regular file - - Fix Pipeline list commit column width should be adjusted - - Close todos when accepting merge requests via the API !6486 (tonygambone) - - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) - - Changed Slack service user referencing from full name to username (Sebastian Poxhofer) - - Retouch environments list and deployments list - - Add multiple command support for all label related slash commands !6780 (barthc) - - Add Container Registry on/off status to Admin Area !6638 (the-undefined) - - Add Nofollow for uppercased scheme in external urls !6820 (the-undefined) - - Allow empty merge requests !6384 (Artem Sidorenko) - - Grouped pipeline dropdown is a scrollable container - - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) - - Fixes padding in all clipboard icons that have .btn class - - Fix a typo in doc/api/labels.md - - Fix double-escaping in activities tab (Alexandre Maia) - - API: all unknown routing will be handled with 404 Not Found - - Add docs for request profiling - - Delete dynamic environments - - Fix buggy iOS tooltip layering behavior. - - Make guests unable to view MRs on private projects - - Fix broken Project API docs (Takuya Noguchi) - - Migrate invalid project members (owner -> master) + +- Fix save button on project pipeline settings page. (!6955) +- All Sidekiq workers now use their own queue +- Avoid race condition when asynchronously removing expired artifacts. (!6881) +- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) +- Respond with 404 Not Found for non-existent tags (Linus Thiel) +- Truncate long labels with ellipsis in labels page +- Improve tabbing usability for sign in page (ClemMakesApps) +- Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint +- Adding members no longer silently fails when there is extra whitespace +- Update runner version only when updating contacted_at +- Add link from system note to compare with previous version +- Use gitlab-shell v3.6.6 +- Ignore references to internal issues when using external issues tracker +- Ability to resolve merge request conflicts with editor !6374 +- Add `/projects/visible` API endpoint (Ben Boeckel) +- Fix centering of custom header logos (Ashley Dumaine) +- Keep around commits only pipeline creation as pipeline data doesn't change over time +- Update duration at the end of pipeline +- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup +- Add group level labels. (!6425) +- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) +- Cancelled pipelines could be retried. !6927 +- Updating verbiage on git basics to be more intuitive +- Fix project_feature record not generated on project creation +- Clarify documentation for Runners API (Gennady Trafimenkov) +- Use optimistic locking for pipelines and builds +- The instrumentation for Banzai::Renderer has been restored +- Change user & group landing page routing from /u/:username to /:username +- Added documentation for .gitattributes files +- Move Pipeline Metrics to separate worker +- AbstractReferenceFilter caches project_refs on RequestStore when active +- Replaced the check sign to arrow in the show build view. !6501 +- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) +- ProjectCacheWorker updates caches at most once per 15 minutes per project +- Fix Error 500 when viewing old merge requests with bad diff data +- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) +- Fix viewing merged MRs when the source project has been removed !6991 +- Speed-up group milestones show page +- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) +- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService +- Fix discussion thread from emails for merge requests. !7010 +- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) +- Add tag shortcut from the Commit page. !6543 +- Keep refs for each deployment +- Close open tooltips on page navigation (Linus Thiel) +- Allow browsing branches that end with '.atom' +- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) +- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) +- Add more tests for calendar contribution (ClemMakesApps) +- Update Gitlab Shell to fix some problems with moving projects between storages +- Cache rendered markdown in the database, rather than Redis +- Add todo toggle event (ClemMakesApps) +- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references +- Simplify Mentionable concern instance methods +- API: Ability to retrieve version information (Robert Schilling) +- Fix permission for setting an issue's due date +- API: Multi-file commit !6096 (mahcsig) +- Unicode emoji are now converted to images +- Revert "Label list shows all issues (opened or closed) with that label" +- Expose expires_at field when sharing project on API +- Fix VueJS template tags being rendered in code comments +- Added copy file path button to merge request diff files +- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) +- Add Issue Board API support (andrebsguedes) +- Allow the Koding integration to be configured through the API +- Add new issue button to each list on Issues Board +- Execute specific named route method from toggle_award_url helper method +- Added soft wrap button to repository file/blob editor +- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) +- Show the time ago a merge request was deployed to an environment +- Add RTL support to markdown renderer (Ebrahim Byagowi) +- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) +- Fix todos page mobile viewport layout (ClemMakesApps) +- Make issues search less finicky +- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) +- Remove redundant mixins (ClemMakesApps) +- Added 'Download' button to the Snippets page (Justin DiPierro) +- Add visibility level to project repository +- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) +- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) +- Fix showing commits from source project for merge request !6658 +- Fix that manual jobs would no longer block jobs in the next stage. !6604 +- Add configurable email subject suffix (Fu Xu) +- Use defined colour for a language when available !6748 (nilsding) +- Added tooltip to fork count on project show page. (Justin DiPierro) +- Use a ConnectionPool for Rails.cache on Sidekiq servers +- Replace `alias_method_chain` with `Module#prepend` +- Enable GitLab Import/Export for non-admin users. +- Preserve label filters when sorting !6136 (Joseph Frazier) +- MergeRequest#new form load diff asynchronously +- Only update issuable labels if they have been changed +- Take filters in account in issuable counters. !6496 +- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) +- Replace static issue fixtures by script !6059 (winniehell) +- Append issue template to existing description !6149 (Joseph Frazier) +- Trending projects now only show public projects and the list of projects is cached for a day +- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) +- Revoke button in Applications Settings underlines on hover. +- Use higher size on Gitlab::Redis connection pool on Sidekiq servers +- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) +- Revert avoid touching file system on Build#artifacts? +- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created +- Add disabled delete button to protected branches (ClemMakesApps) +- Add broadcast messages and alerts below sub-nav +- Better empty state for Groups view +- API: New /users/:id/events endpoint +- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) +- Replace bootstrap caret with fontawesome caret (ClemMakesApps) +- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 +- Add organization field to user profile +- Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being. +- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) +- Fix deploy status responsiveness error !6633 +- Make searching for commits case insensitive +- Fix resolved discussion display in side-by-side diff view !6575 +- Optimize GitHub importing for speed and memory +- API: expose pipeline data in builds API (!6502, Guilherme Salazar) +- Notify the Merger about merge after successful build (Dimitris Karakasilis) +- Reduce queries needed to find users using their SSH keys when pushing commits +- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) +- Fix broken repository 500 errors in project list +- Fix the diff in the merge request view when converting a symlink to a regular file +- Fix Pipeline list commit column width should be adjusted +- Close todos when accepting merge requests via the API !6486 (tonygambone) +- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) +- Changed Slack service user referencing from full name to username (Sebastian Poxhofer) +- Retouch environments list and deployments list +- Add multiple command support for all label related slash commands !6780 (barthc) +- Add Container Registry on/off status to Admin Area !6638 (the-undefined) +- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined) +- Allow empty merge requests !6384 (Artem Sidorenko) +- Grouped pipeline dropdown is a scrollable container +- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) +- Fixes padding in all clipboard icons that have .btn class +- Fix a typo in doc/api/labels.md +- Fix double-escaping in activities tab (Alexandre Maia) +- API: all unknown routing will be handled with 404 Not Found +- Add docs for request profiling +- Delete dynamic environments +- Fix buggy iOS tooltip layering behavior. +- Make guests unable to view MRs on private projects +- Fix broken Project API docs (Takuya Noguchi) +- Migrate invalid project members (owner -> master) ## 8.12.7 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 4f2c1d15f6d..fcdb2e109f6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.6.6 +4.0.0 @@ -51,7 +51,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.6.8' +gem 'gitlab_git', '~> 10.7.0' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -161,6 +161,9 @@ gem 'connection_pool', '~> 2.0' # HipChat integration gem 'hipchat', '~> 1.5.0' +# JIRA integration +gem 'jira-ruby', '~> 1.1.2' + # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' @@ -326,7 +329,7 @@ gem 'newrelic_rpm', '~> 3.16' gem 'octokit', '~> 4.3.0' -gem 'mail_room', '~> 0.8.1' +gem 'mail_room', '~> 0.9.0' gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index 442184b9228..81af1ad2dac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.0) - gitlab_git (10.6.8) + gitlab_git (10.7.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -356,6 +356,9 @@ GEM cause json ipaddress (0.8.3) + jira-ruby (1.1.2) + activesupport + oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) @@ -402,7 +405,7 @@ GEM systemu (~> 2.6.2) mail (2.6.4) mime-types (>= 1.16, < 4) - mail_room (0.8.1) + mail_room (0.9.0) method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) @@ -421,7 +424,7 @@ GEM mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) numerizer (0.1.1) - oauth (0.4.7) + oauth (0.5.1) oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) @@ -867,7 +870,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.0) - gitlab_git (~> 10.6.8) + gitlab_git (~> 10.7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -881,6 +884,7 @@ DEPENDENCIES html-pipeline (~> 1.11.0) httparty (~> 0.13.3) influxdb (~> 0.2) + jira-ruby (~> 1.1.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) jquery-turbolinks (~> 2.1.0) @@ -893,7 +897,7 @@ DEPENDENCIES license_finder (~> 2.1.0) licensee (~> 8.0.0) loofah (~> 2.0.3) - mail_room (~> 0.8.1) + mail_room (~> 0.9.0) method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) @@ -994,4 +998,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.2 + 1.13.5 diff --git a/README.md b/README.md index a6b30aff5a0..46492f4b9c8 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,10 @@ There are various other options to install GitLab, please refer to the [installa You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password. +## Contributing + +GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. + ## 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). diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 2fe46b9fd06..82e526ae0ef 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index e9287d015c3..59ac9b9cef5 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Activities = (function() { function Activities() { diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index f8460beb5d2..1ef340e4ca1 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Admin = (function() { function Admin() { diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 56ec1489f89..7ebe1599fca 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Api = { groupsPath: "/api/:version/groups.json", diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 879f90bdc05..7dd9adac736 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,5 +1,6 @@ +/* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. @@ -187,6 +188,7 @@ // Close select2 on escape }); // Initialize tooltips + $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', placement: function(_, el) { diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 7b546e79ee0..c7eff27f971 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Aside = (function() { function Aside() { diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index a9aec6e8ea4..ab09e4475e6 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Autosave = (function() { function Autosave(field, key) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index d5966db0bf9..8bdb0965f99 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.AwardsHandler = (function() { const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence @@ -91,7 +92,7 @@ css = { top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" }; - if ((position != null) && position === 'right') { + if (position === 'right') { css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; $menu.addClass('is-aligned-right'); } else { diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index dc8ae601961..074378b9e52 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require jquery.ba-resize */ /*= require autosize */ diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 1df681a4816..a64cefb62bd 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { $(function() { $("body").on("click", ".js-details-target", function() { @@ -14,6 +15,11 @@ return $("body").on("click", ".js-details-expand", function(e) { $(this).next('.js-details-content').removeClass("hide"); $(this).hide(); + + var truncatedItem = $(this).siblings('.js-details-short'); + if (truncatedItem.length) { + truncatedItem.addClass("hide"); + } return e.preventDefault(); }); }); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 54b7360ab41..7ff88ecdcaf 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Quick Submit behavior // // When a child field of a form with a `js-quick-submit` class receives a diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 894034bdd54..4ac343f876c 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Requires Input behavior // // When called on a form with input fields with the `required` attribute, the diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index a6ce378d67a..05b213fe3fb 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function(w) { $(function() { // Toggle button. Show/hide content inside parent container. diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index d6ea4f84f57..37531aaec9b 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 8cca1aa9232..33fb4f8185c 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.BlobFileDropzone = (function() { function BlobFileDropzone(form, method) { diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index cd746b05cf6..344fe5dcd94 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 4e9500428b2..9e992f7913c 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.BlobGitignoreSelectors = (function() { function BlobGitignoreSelectors(opts) { diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 2701df3e6de..41a83a56146 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require blob/template_selector */ diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 index 153ed457559..adeb8ba1318 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class BlobLicenseSelectors { constructor({ $dropdowns, editor }) { diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 2d5c6ade053..5434a19bcec 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index 2afef43f3d6..b801c10f168 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 8db4f6a3b28..60840560dd3 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 56f91503017..efb22d38513 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require vue //= require vue-resource //= require Sortable diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index 500213a3a43..0e03d43872b 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require ./board_blank_state //= require ./board_delete //= require ./board_list diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index ff90f2d6d75..885553690d3 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index f743d72f936..2f6c03e3538 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6 index 34653cd48ef..c45e1926c5c 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 7022a29e818..34fc7694241 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require ./board_card //= require ./board_new_issue diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index b31adfdb8da..7fc0bfd56f3 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index e83e69247d9..4928320d015 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index 6ccd83e2d84..fe1a6dc7ea0 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ $(() => { const Store = gl.issueBoards.BoardsStore; @@ -32,6 +33,7 @@ $(() => { }, filterable: true, selectable: true, + multiSelect: true, clicked (label, $el, e) { e.preventDefault(); diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 50ef1911022..9eceac4eddd 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index bd9ba7d5118..e520170ef74 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 03fd6c05d87..21d735e8231 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class ListIssue { constructor (obj) { this.id = obj.iid; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index 583829552cd..0910fe9a854 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class ListLabel { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 5d0a561cdba..b331a26fed5 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class List { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 index 577adf11265..a48969e19c9 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class ListMilestone { constructor (obj) { this.id = obj.id; diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6 index 904b3a68507..583a973fc46 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class ListUser { constructor (user) { this.id = user.id; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index b76063911bb..f59a2ed7937 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class BoardService { constructor (root, boardId) { this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index c30f66e3a5a..175e034afed 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -62,7 +63,8 @@ this.removeList('blank'); Cookies.set('issue_board_welcome_hidden', 'true', { - expires: 365 * 10 + expires: 365 * 10, + path: '' }); }, welcomeIsHidden () { diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js index 75f8b730195..039ca491cf5 100644 --- a/app/assets/javascripts/boards/test_utils/simulate_drag.js +++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function () { 'use strict'; diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index b5ff3a81ed5..80f137ca12e 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 5fef9725178..5d4d23e26c6 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Breakpoints = (function() { var BreakpointInstance, instance; diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index fceeff36728..576f4c76c1e 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { $(function() { var previewPath; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index f4c387a1a05..12e653f4122 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index f345ba0abe6..49f84581650 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.BuildArtifacts = (function() { function BuildArtifacts() { diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 index 8d3e29794a1..0ecd20bc11e 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ $(function(){ $('.reveal-variables').off('click').on('click',function(){ $('.js-build').toggle().niceScroll(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 23cf5b519f4..fac5b4f17da 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Commit = (function() { function Commit() { diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index be24ee56aad..16d63729d31 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.CommitFile = (function() { function CommitFile(file) { diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index e893491b19b..ffddce1297b 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ImageFile = (function() { var prepareFrames; diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 9132089adcd..c765d233831 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.CommitsList = (function() { function CommitsList() {} diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 342ac0e8e69..b3f769d4129 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Compare = (function() { function Compare(opts) { diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index 9a2082d97e0..bd980f87e72 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.CompareAutocomplete = (function() { function CompareAutocomplete() { diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 708ab08ffac..230a1b95c52 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ConfirmDangerModal = (function() { function ConfirmDangerModal(form, text) { diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index e23bda2fa4e..7808d7fe313 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require clipboard */ diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index c5f8c29242d..f20580b1279 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (function (w) { class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 43c60d25248..331f0209888 100644 --- a/app/assets/javascripts/cycle_analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require vue ((global) => { diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 8086c10ad6b..4ddafff428f 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Diff = (function() { var UNFOLD_COUNT; diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index 48bc7d77805..29a12a2395b 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.CommentAndResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index ad80d1118df..983e554b9c1 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (() => { JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index cdedfd1af15..bcc052c7c8c 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.ResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index 9e383b14a3e..24a99e23132 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index 0a617034502..060034f049b 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.ResolveDiscussionBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index 22d9cf6c857..6149bfd052a 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require vue //= require vue-resource //= require_directory ./models diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index a05f885201d..7a929017f36 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.DiscussionMixins = { computed: { diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 index 488714e4870..439f55520ef 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class DiscussionModel { constructor (discussionId) { this.id = discussionId; diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 index f2d2d389c38..d0541b02632 100644 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ b/app/assets/javascripts/diff_notes/models/note.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class NoteModel { constructor (discussionId, noteId, canResolve, resolved, resolved_by) { this.discussionId = discussionId; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 2a55f739b31..86953ce7ffb 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { class ResolveServiceClass { constructor() { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index 69522e1dac5..f42ca406bb1 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.CommentsStore = { state: {}, diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index a1fe57562fa..ff8b8f6d0ae 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var Dispatcher; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 4a6fea929c7..1a0aa9757ba 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require preview_markdown */ diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index 27a7b95e281..fd7f961aab9 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (function(global) { class DueDateSelect { constructor({ $dropdown, $loading } = {}) { diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js index 24f9e00097c..4c9e219aa43 100644 --- a/app/assets/javascripts/extensions/array.js +++ b/app/assets/javascripts/extensions/array.js @@ -1,3 +1,4 @@ +/* eslint-disable */ Array.prototype.first = function() { return this[0]; } diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index d5d4af3573c..c74fc9ad074 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches; Element.prototype.closest = function closest(selector, selectedElement = this) { diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index 4978e24949c..623a80b7053 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Disable an element and add the 'disabled' Bootstrap class (function() { $.fn.extend({ diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 3fb3b1a8b51..732136f1f2c 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index c8a02d6fa15..46e272c3311 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Flash = (function() { var hideFlash; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 845313b6b38..31df51ac03a 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ // Creates the variables for setting up GFM auto-completion (function() { if (window.GitLab == null) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 167979a55d9..98e43c4d088 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, @@ -208,7 +209,7 @@ FILTER_INPUT = '.dropdown-input .dropdown-input-field'; function GitLabDropdown(el1, options) { - var ref, ref1, ref2, ref3, searchFields, selector, self; + var searchFields, selector, self; this.el = el1; this.options = options; this.updateLabel = bind(this.updateLabel, this); @@ -219,7 +220,11 @@ selector = $(this.el).data("target"); this.dropdown = selector != null ? $(selector) : $(this.el).parent(); // Set Defaults - ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true; + this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); + this.highlight = !!this.options.highlight + this.filterInputBlur = this.options.filterInputBlur != null + ? this.options.filterInputBlur + : true; // If no input is passed create a default one self = this; // If selector was passed @@ -234,6 +239,7 @@ this.fullData = this.options.data; currentIndex = -1; this.parseData(this.options.data); + this.focusTextInput(); } else { this.remote = new GitLabDropdownRemote(this.options.data, { dataType: this.options.dataType, @@ -242,6 +248,7 @@ return function(data) { _this.fullData = data; _this.parseData(_this.fullData); + _this.focusTextInput(); if (_this.options.filterable && _this.filter && _this.filter.input) { return _this.filter.input.trigger('input'); } @@ -418,7 +425,9 @@ var $target; if (this.options.multiSelect) { $target = $(e.target); - if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { + if ($target && !$target.hasClass('dropdown-menu-close') && + !$target.hasClass('dropdown-menu-close-icon') && + !$target.data('is-link')) { e.stopPropagation(); return false; } else { @@ -445,9 +454,8 @@ contentHtml = $('.dropdown-content', this.dropdown).html(); if (this.remote && contentHtml === "") { this.remote.execute(); - } - if (this.options.filterable) { - this.filterInput.focus(); + } else { + this.focusTextInput(); } if (this.options.showMenuAbove) { @@ -634,7 +642,9 @@ } field = []; - value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; + value = this.options.id + ? this.options.id(selectedObject, el) + : selectedObject.id; if (isInput) { field = $(this.el); } else if(value) { @@ -682,6 +692,10 @@ return selectedObject; }; + GitLabDropdown.prototype.focusTextInput = function() { + if (this.options.filterable) { this.filterInput.focus() } + } + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { var $input; // Create hidden input for form diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 8e8f9f29ab3..be6c3ec274f 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { /* * This class overrides the browser's validation error bubbles, displaying custom diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 2703adc0705..742807d93ad 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.GLForm = (function() { function GLForm(form) { diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 4886da9f21f..e103748d499 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,5 +1,6 @@ +/* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js index f041980bc19..b796a9abb49 100644 --- a/app/assets/javascripts/graphs/stat_graph.js +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.StatGraph = (function() { function StatGraph() {} diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 927d241b357..818bff0c413 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require d3 */ diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 7d9d4d7c679..dea26a3f1e1 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require d3 */ @@ -29,8 +30,7 @@ ContributorsGraph.set_y_domain = function(data) { return ContributorsGraph.prototype.y_domain = [ 0, d3.max(data, function(d) { - var ref, ref1; - return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return d.commits = d.commits || d.additions || d.deletions; }) ]; }; @@ -44,8 +44,7 @@ ContributorsGraph.init_y_domain = function(data) { return ContributorsGraph.prototype.y_domain = [ 0, d3.max(data, function(d) { - var ref, ref1; - return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return d.commits = d.commits || d.additions || d.deletions; }) ]; }; @@ -147,9 +146,8 @@ return this.area = d3.svg.area().x(function(d) { return x(d.date); }).y0(this.height).y1(function(d) { - var ref, ref1, xa; - xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; - return y(xa); + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); }).interpolate("basis"); }; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 0d240bed8b6..362a77e868f 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { window.ContributorsStatGraphUtil = { parse_log: function(log) { diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index c28ce86d7af..774477dc7a9 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.GroupAvatar = (function() { function GroupAvatar() { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 5f06186504b..b275620c799 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var slice = [].slice; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js new file mode 100644 index 00000000000..81fcaf06430 --- /dev/null +++ b/app/assets/javascripts/header.js @@ -0,0 +1,10 @@ +/* eslint-disable */ +(function() { + + $(document).on('todo:toggle', function(e, count) { + var $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(gl.text.addDelimiter(count)); + $todoPendingCount.toggleClass('hidden', count === 0); + }); + +})(); diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 4aced1e618f..c53f7c88aa2 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ImporterStatus = (function() { function ImporterStatus(jobs_url, import_url) { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 54056ac50c8..8fc498be27d 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var issuable_created; diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 8147e83ffe8..fae49ee6144 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index b7f92ae9883..849b45756ee 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -44,8 +45,8 @@ }; IssuableForm.prototype.handleSubmit = function() { - var ref, ref1; - if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) { + var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null; + if ((parseInt(fieldId) || 0) > 0) { if (!confirm(this.issueMoveConfirmMsg)) { return false; } diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 261bf6137c2..67ace697936 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require flash */ /*= require jquery.waitforimages */ @@ -94,7 +95,11 @@ return $.ajax({ type: 'PATCH', url: $('form.js-issuable-update').attr('action'), - data: patchData + data: patchData, + success: function(issue) { + document.querySelector('#task_status').innerText = issue.task_status; + document.querySelector('#task_status_short').innerText = issue.task_status_short; + } }); // TODO (rspeicher): Make the issue description inline-editable like a note so // that we can re-use its form here diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 076e3972944..d7262e5eb74 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.IssueStatusSelect = (function() { function IssueStatusSelect() { diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 0808f538f01..9697fb33566 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class IssuableBulkActions { diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index bc68e53504f..175623e7448 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class LabelManager { diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index cb16e2ba814..3033e8ca5c2 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 4e29ab71bd4..c334e3e0c02 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.LabelsSelect = (function() { function LabelsSelect() { diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 8e2fc0d1479..6b4edf02f4d 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var hideEndFade; diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index 4cdf99cae72..b1718e89d3d 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,2 +1,3 @@ +/* eslint-disable */ /*= require ace-rails-ap */ /*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index d9b07c10a49..e1dfdae97de 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require Chart */ diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index a88e640f298..155e30cc462 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require cropper */ diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index ee1baf54803..0c9c2787077 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require d3 */ diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index 6df427bc2b1..cc445db274b 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require raphael */ /*= require g.raphael */ diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js index d36efdabc93..a68edab2aad 100644 --- a/app/assets/javascripts/lib/utils/animate.js +++ b/app/assets/javascripts/lib/utils/animate.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { if (w.gl == null) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 698abae6228..21efe2d76dd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var base; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 8fdf4646cd8..59e526ed623 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var base; diff --git a/app/assets/javascripts/lib/utils/jquery.timeago.js b/app/assets/javascripts/lib/utils/jquery.timeago.js index cc17aa7d3d1..de76cdd2ea7 100644 --- a/app/assets/javascripts/lib/utils/jquery.timeago.js +++ b/app/assets/javascripts/lib/utils/jquery.timeago.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /** * Timeago is a jQuery plugin that makes it easy to support automatically * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 5b338b00d76..dafc006d2e5 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var notificationGranted, notifyMe, notifyPermissions; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d761a844be9..98f9815ff05 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var base; @@ -7,6 +8,9 @@ if ((base = w.gl).text == null) { base.text = {}; } + gl.text.addDelimiter = function(text) { + return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; + } gl.text.randomString = function() { return Math.random().toString(36).substring(7); }; diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index dc30babd645..4fd1e3fc1d3 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var base; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index b8d52becb3f..44a66a915e3 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { (function(w) { var base; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 93daea1dce7..ea5a60bb78e 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // LineHighlighter // // Handles single- and multi-line selection and highlight for blob views. diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 7d8eef1b495..d4f86534f0c 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { Turbolinks.enableProgressBar(); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index e1532fd9ec4..0bd90c57396 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index 2bdd0f7a637..371abd09e78 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((w) => { w.gl = w.gl || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 index 5012bdfe997..6da3942ea52 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 index b4be1c8988d..23c4618af70 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 index 8b0a8ab2073..797850262cc 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 index eb4cc6a9dac..1b3e9901f1e 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 index da2fb8b1323..8a7519b0786 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index 791db57262f..f94e51e783c 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 7fd3749b3e2..222a5dcfc2e 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 index 114a2c5b305..c8de586aa21 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 index b846a90ab2a..88c3a20ce13 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 02ff5a382e2..d3bd1e846c1 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require jquery.waitforimages */ /*= require task_list */ @@ -96,7 +97,11 @@ return $.ajax({ type: 'PATCH', url: $('form.js-issuable-update').attr('action'), - data: patchData + data: patchData, + success: function(mergeRequest) { + document.querySelector('#task_status').innerText = mergeRequest.task_status; + document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short; + } }); // TODO (rspeicher): Make the merge request description inline-editable like a // note so that we can re-use its form here diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 18cc4d4ca93..6658e4811ce 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // MergeRequestTabs // // Handles persisting and restoring the current tab selection and lazily-loading diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 3ff6851d59b..3a2fe454b68 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 1fed38661a2..7ad86d8c084 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index bc1a99057d9..9299c96e8ea 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Milestone = (function() { Milestone.updateIssue = function(li, issue_url, data) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 95b5b934c81..c909b53dc21 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject) { diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 10f4fd106d8..d1168227b77 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 91132af273a..74dbeb94741 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 7baebcd100a..8898e7ace43 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Network = (function() { function Network(opts) { diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 67c3e645364..42d6799c82f 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,5 +1,6 @@ +/* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 20aa2fced27..0e643b0ff14 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 21bf8867f7b..acb529023fa 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 866a04d3e21..4976eef2896 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require autosave */ /*= require autosize */ diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index a41e9d3fabe..ef3f2c6ae73 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.NotificationsDropdown = (function() { function NotificationsDropdown() { diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 6b2ef17ef6b..6fbec8efe9b 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index b81ed50cb48..2e4dc62273e 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Pager = { init: function(limit, preload, disable, callback) { diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 0fa56df0d2a..e6fada5c84c 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class Pipelines { diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 5200487814f..f2a45a18bed 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // MarkdownPreview // // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index a1b0126e857..6da6c1d0295 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index b2307be73ad..73858388261 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class Profile { diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index d6e4d9f7ad8..22bee0f6187 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 61c33dc1f20..2d0c6b16699 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index 277e71523d5..61877c6616d 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectAvatar = (function() { function ProjectAvatar() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index b8347367717..ddac5ed83e1 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index d2261c51f35..fd95f8f2c19 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectFork = (function() { function ProjectFork() { diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index c61b0cf2fde..f1c4a9fe542 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectImport = (function() { function ProjectImport() { diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 478e82aa14d..40575caa57f 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 4239ed2f889..b74b4ae68ff 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectSelect = (function() { function ProjectSelect() { diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index c8cfc9a9ba8..21650f5f67a 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectShow = (function() { function ProjectShow() {} diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 04fb49552e8..3458cd89ae2 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.ProjectsList = { init: function() { diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 7aeb5f92514..2d60947a666 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index 46beca469b9..c45c9d8ff22 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index 983322cbecc..e3f226e9a2a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index 15a6dca2875..ac3142ffb07 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index 9ff0fd12c76..705378a364d 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 15b3affd469..17e34163831 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1 +1,2 @@ +/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 870ddfe05f1..df38937858f 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -81,16 +82,11 @@ }; Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) { - var $todoPendingCount; - $todoPendingCount = $('.todos-pending-count'); - $todoPendingCount.text(data.count); + $(document).trigger('todo:toggle', data.count); + $btn.enable(); $todoLoading.addClass('hidden'); - if (data.count === 0) { - $todoPendingCount.addClass('hidden'); - } else { - $todoPendingCount.removeClass('hidden'); - } + if (data.delete_path != null) { $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); return $btnText.text($btn.data('mark-text')); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 8074a94f33e..6c2389f202f 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Search = (function() { function Search() { diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index b4c6226dc68..5fa94556501 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { const KEYCODE = { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 3aa8536d40a..8d8ab6dda5e 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index b931eab638f..704a8bd3a57 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index f7492a2aa5c..befe4eccdba 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 92ce31969e3..90ed4267661 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 235bf4f95ec..25ec7dbc067 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require mousetrap */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index b04159420d1..19c6b7d30ab 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts */ diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index fb2b39e757e..002e979a2c6 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts_navigation */ diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 73c359af7f6..a23ca449c4b 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { let singleton; @@ -37,7 +38,8 @@ .on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', 'html, body', (e) => this.handleClickEvent(e)) - .on('page:change', () => this.renderState()); + .on('page:change', () => this.renderState()) + .on('todo:toggle', (e, count) => this.updateTodoCount(count)); this.renderState(); } @@ -52,6 +54,10 @@ } } + updateTodoCount(count) { + $('.js-todos-count').text(gl.text.addDelimiter(count)); + } + toggleSidebar() { this.isExpanded = !this.isExpanded; this.renderState(); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index ee6af123268..adca76ddd5f 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 855e97eb301..083dc23c796 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 index 6f0996c0d2a..c3afc3f2246 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ (global => { global.gl = global.gl || {}; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 10509313c12..a18d16ea46c 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.Star = (function() { function Star() { diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js index bfa3663bca3..f9915593657 100644 --- a/app/assets/javascripts/subscription.js +++ b/app/assets/javascripts/subscription.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index d6c219603d1..2ca65cb762d 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.SubscriptionSelect = (function() { function SubscriptionSelect() { diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 2ae7bf5fc15..77ad4f30b7a 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Syntax Highlighter // // Applies a syntax highlighting color scheme CSS class to any element with the diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index fa1b79c8415..93a3d67ee9f 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require ../blob/template_selector */ ((global) => { diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index 4e8247b89e1..0a3890e85fe 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class IssuableTemplateSelectors { constructor({ $dropdowns, editor } = {}) { diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 055228c5df8..213e80825b7 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { class Todos { @@ -97,7 +98,8 @@ } updateBadges(data) { - $('.todos-pending .badge, .todos-pending-count').text(data.count); + $(document).trigger('todo:toggle', data.count); + $('.todos-pending .badge').text(data.count); return $('.todos-done .badge').text(data.done_count); } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 9b7be17c4fe..70aff4b9a2f 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.TreeView = (function() { function TreeView() { diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index ce2930c7fc7..35f2b1e2b25 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index bc48c67c4f2..aff605169e4 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 926912fa988..22fbf9f3a91 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 907e640161a..2eab2d5ae23 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { this.U2FUtil = (function() { function U2FUtil() {} diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 111ea7b2ec4..5e869e99fdb 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { global.User = class { constructor({ action }) { diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index dfdfa1e7f75..2b310da319c 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /* UserTabs diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index bf4b2e320cd..c4dde575c6e 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ ((global) => { const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 3bd4c3c066f..0ec878e7e60 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index d6e4d9f7ad8..22bee0f6187 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require_tree . */ diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index da6a59bcf97..3847278e80a 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice; diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index 35401231fbf..ad9b842db3c 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require latinise */ diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 777b32b41c9..fa124e7052d 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Zen Mode (full screen) textarea // /*= provides zen_mode:enter */ diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c0e9c8bf829..ed21ad83a1c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -216,7 +216,7 @@ svg, .fa { &:not(:last-child) { - margin-right: 3px; + margin-right: 5px; } } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1de246600fd..baa38ab60c8 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -15,6 +15,7 @@ @media (max-width: $screen-xs-max) { width: 100%; + min-width: 240px; } } @@ -485,7 +486,7 @@ font-size: 20px; text-indent: 0; - &:before { + &::before { display: block; position: relative; top: -2px; @@ -517,7 +518,7 @@ background-color: transparent; border: 0; - .ui-icon:before { + .ui-icon::before { color: $md-link-color; } } @@ -526,7 +527,7 @@ .ui-datepicker-prev { left: 0; - .ui-icon:before { + .ui-icon::before { content: '\f104'; text-align: left; } @@ -535,7 +536,7 @@ .ui-datepicker-next { right: 0; - .ui-icon:before { + .ui-icon::before { content: '\f105'; text-align: right; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 48e34a0066e..78464af94bd 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -14,7 +14,7 @@ border-bottom: 1px solid #eee; border-bottom: 1px solid rgba(0, 0, 0, 0.05); - &:after { + &::after { content: " "; display: table; clear: both; @@ -38,7 +38,7 @@ &.smoke { background-color: $background-color; } - &:hover { + &:not(.ui-sort-disabled):hover { background: $row-hover; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 1d8e64a0e4b..c54f7b27575 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -164,6 +164,18 @@ padding-left: $sidebar_width; } } + + .merge-request-tabs-holder.affix { + @media (min-width: $sidebar-breakpoint) { + left: $sidebar_width; + } + } + + &.right-sidebar-expanded { + .line-resolve-all-container { + display: none; + } + } } header.header-sidebar-pinned { diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index eb63a9f214b..875cded8b4e 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -45,7 +45,7 @@ @media (max-width: $screen-xs-max) { .timeline { - &:before { + &::before { background: none; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 266a8024809..070e42d63d2 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -148,7 +148,7 @@ a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { - &:before { + &::before { margin-right: 4px; font: normal normal normal 14px/1 FontAwesome; @@ -158,13 +158,13 @@ content: "\f0c6"; } - &:hover:before { + &:hover::before { text-decoration: none; } } a.no-attachment-icon { - &:before { + &::before { display: none; } } @@ -183,13 +183,13 @@ position: absolute; text-decoration: none; - &:after { + &::after { content: image-url('icon_anchor.svg'); visibility: hidden; } } - &:hover > a.anchor:after { + &:hover > a.anchor::after { visibility: visible; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b271f8cf332..be2a7ceefff 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -90,6 +90,8 @@ $table-border-color: #f0f0f0; $background-color: $gray-light; $dark-background-color: #f5f5f5; $table-text-gray: #8f8f8f; +$widget-expand-item: #e8f2f7; +$widget-inner-border: #eef0f2; /* * Text diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 63396a6bb29..6cefafd8fc7 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -80,10 +80,13 @@ display: -webkit-flex; display: -ms-flexbox; display: flex; + white-space: nowrap; } .user-details { flex: 1 1 auto; + overflow: hidden; + padding-right: 8px; } .user-name { @@ -91,6 +94,12 @@ font-weight: 600; } + .user-name, + .user-email { + overflow: hidden; + text-overflow: ellipsis; + } + .dropdown { .btn-block { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 9282e0ae03b..486ad16ea26 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,7 +1,7 @@ .awards { .emoji-icon { - width: 20px; - height: 20px; + width: 19px; + height: 19px; } } @@ -94,7 +94,7 @@ .award-control { margin: 3px 5px 3px 0; - padding: 6px 5px; + padding: 5px 6px; outline: 0; &:hover, @@ -127,7 +127,7 @@ .award-control-icon { float: left; margin-right: 5px; - font-size: 20px; + font-size: 19px; } .award-control-icon-loading { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 8ecac08137b..8ecf7fcb96d 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -33,10 +33,8 @@ &.commit-info-row-header { line-height: 34px; - - @media (min-width: $screen-sm-min) { - margin-bottom: 0; - } + padding: 10px 0; + margin-bottom: 0; .commit-options-dropdown-caret { @media (max-width: $screen-sm) { @@ -80,6 +78,58 @@ } } +.js-details-expand { + &:hover { + text-decoration: none; + } +} + +.commit-info-widget { + background: $background-color; + color: $gl-gray; + border: 1px solid $border-color; + border-radius: $border-radius-default; + + .widget-row { + padding: $gl-padding; + + &:not(:last-of-type) { + border-bottom: 1px solid $widget-inner-border; + } + + &.branch-info { + .monospace, + .commit-info { + margin-left: 4px; + } + } + } + + .icon-container { + display: inline-block; + margin-right: 8px; + + svg { + position: relative; + top: 2px; + height: 16px; + width: 16px; + } + + &.commit-icon { + svg { + path { + fill: $gl-text-color; + } + } + } + } + + .label.label-gray { + background-color: $widget-expand-item; + } +} + .ci-status-link { svg { overflow: visible; @@ -88,6 +138,7 @@ .commit-box { border-top: 1px solid $border-color; + padding: $gl-padding 0; .commit-title { margin: 0; @@ -138,6 +189,9 @@ } .commit-action-buttons { + position: relative; + top: -1px; + i { color: $gl-icon-color; font-size: 13px; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index ad315cfae62..52d6a39bd59 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -33,21 +33,22 @@ color: $gl-dark-link-color; } - .text-expander { - display: inline-block; - background: $gray-light; - color: $gl-placeholder-color; - padding: 0 5px; - cursor: pointer; - border: 1px solid $border-gray-dark; - border-radius: $border-radius-default; - margin-left: 5px; - line-height: 1; - - &:hover { - background-color: darken($gray-light, 10%); - text-decoration: none; - } +} + +.text-expander { + display: inline-block; + background: $gray-light; + color: $gl-placeholder-color; + padding: 0 5px; + cursor: pointer; + border: 1px solid $border-gray-dark; + border-radius: $border-radius-default; + margin-left: 5px; + line-height: 1; + + &:hover { + background-color: darken($gray-light, 10%); + text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e0367d1d942..fde138c874d 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -94,14 +94,14 @@ position: relative; &.old { - &:before { + &::before { content: '-'; position: absolute; } } &.new { - &:before { + &::before { content: '+'; position: absolute; } @@ -471,7 +471,7 @@ .file-holder { .diff-line-num:not(.js-unfold-bottom) { a { - &:before { + &::before { content: attr(data-linenumber); } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 3d2b024fe5c..a2f5c6c6bd3 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -54,7 +54,6 @@ margin: 0 0 10px; } - .login-footer { margin-top: 10px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 70afa568554..f8e31a624ec 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -60,7 +60,7 @@ } .ci_widget { - border-bottom: 1px solid #eef0f2; + border-bottom: 1px solid $widget-inner-border; svg { margin-right: 4px; @@ -279,6 +279,10 @@ #modal_merge_info .modal-dialog { width: 600px; + .dark { + margin-right: 40px; + } + .btn-clipboard { @extend .pull-right; @@ -439,12 +443,12 @@ } .merge-request-tabs-holder { - background-color: #fff; + background-color: $white-light; &.affix { top: 100px; left: 0; - z-index: 9; + z-index: 10; transition: right .15s; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index b90c91831f2..526e9ae5cdd 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -105,11 +105,6 @@ ul.notes { padding: 2px; margin-top: 10px; } - - .award-control { - font-size: 13px; - padding: 2px 5px; - } } .note-header { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f88175365c6..a8e8bbcb208 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -474,8 +474,8 @@ } .arrow { - &:before, - &:after { + &::before, + &::after { content: ''; display: inline-block; position: absolute; @@ -486,14 +486,14 @@ top: 18px; } - &:before { + &::before { left: -5px; margin-top: -6px; border-width: 7px 5px 7px 0; border-right-color: $border-color; } - &:after { + &::after { left: -4px; margin-top: -9px; border-width: 10px 7px 10px 0; @@ -573,8 +573,7 @@ .build { // Remove right connecting horizontal line from first build in last stage &:first-child { - &::after, - &::before { + &::after { border: none; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 3f6fdaebc1d..ede29db1979 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -77,14 +77,14 @@ // Middle dot divider between each element in a list of items. .middle-dot-divider { - &:after { + &::after { content: "\00B7"; // Middle Dot padding: 0 6px; font-weight: bold; } &:last-child { - &:after { + &::after { content: ""; padding: 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f6355941837..f0d39b353d2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -193,7 +193,7 @@ margin-left: 4px; .arrow { - &:before { + &::before { content: ''; display: inline-block; position: absolute; @@ -209,7 +209,7 @@ pointer-events: none; } - &:after { + &::after { content: ''; position: absolute; width: 0; @@ -351,7 +351,7 @@ a.deploy-project-label { line-height: 36px; margin: 0; - > li + li:before { + > li + li::before { padding: 0 3px; color: #999; } @@ -790,7 +790,7 @@ pre.light-well { top: 7px; color: $location-icon-color; - &:before { + &::before { font-family: FontAwesome; font-weight: normal; font-style: normal; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 6d472e8293f..bf688af50e2 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -72,7 +72,7 @@ top: 0; color: $location-icon-color; - &:before { + &::before { font-family: FontAwesome; font-weight: normal; font-style: normal; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index ea76fe18876..b3aef2fdd32 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -161,3 +161,63 @@ } } } + +.todos-empty { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + max-width: 900px; + margin-left: auto; + margin-right: auto; + + @media (min-width: $screen-sm-min) { + -webkit-flex-direction: row; + flex-direction: row; + padding-top: 80px; + } +} + +.todos-empty-content { + -webkit-align-self: center; + align-self: center; + max-width: 480px; + margin-right: 20px; +} + +.todos-empty-hero { + width: 200px; + margin-left: auto; + margin-right: auto; + + @media (min-width: $screen-sm-min) { + width: 300px; + margin-right: 0; + -webkit-order: 2; + order: 2; + } +} + +.todos-all-done { + padding-top: 20px; + + @media (min-width: $screen-sm-min) { + padding-top: 50px; + } + + > svg { + display: block; + max-width: 300px; + margin: 0 auto 20px; + } + + p { + max-width: 470px; + margin-left: auto; + margin-right: auto; + } + + a { + font-weight: 600; + } +} diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 587bd6a1e8a..e73cecc92be 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -5,7 +5,7 @@ } .example { - &:before { + &::before { content: "Example"; color: #bbb; } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 8239b7e6879..0ff3c3f5472 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -35,7 +35,7 @@ nav.navbar-collapse.collapse, .nav, .btn, ul.notes-form, -.merge-request-ci-status .ci-status-link:after, +.merge-request-ci-status .ci-status-link::after, .issuable-gutter-toggle, .gutter-toggle, .issuable-details .content-block-small, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f35f4a8c811..bb912ed10cc 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -3,7 +3,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.order_name_asc.filter(params[:filter]) - @users = @users.search(params[:name]) if params[:name].present? + @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? @users = @users.sort(@sort = params[:sort]) @users = @users.page(params[:page]) end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 4cb3be41064..c33d7eecb9f 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -18,7 +18,7 @@ module ServiceParams :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id] + :jira_issue_transition_id, :url, :project_key] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cb649264146..3f1a1d1c511 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -112,7 +112,7 @@ class Projects::IssuesController < Projects::ApplicationController end format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) + render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 4f855134368..42fd09e9b7e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -126,7 +126,7 @@ class Projects::LabelsController < Projects::ApplicationController alias_method :subscribable_resource, :label def find_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities) + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def authorize_admin_labels! diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index eff79b63cee..ac0a569d72a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -278,7 +278,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.target_project, @merge_request]) end format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) + render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) end end else diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d08f490de18..699a56ae2f8 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -25,18 +25,15 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - if params[:user_ids].blank? - return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.') - end + status = Members::CreateService.new(@project, current_user, params).execute - @project.team.add_users( - params[:user_ids].split(','), - params[:access_level], - expires_at: params[:expires_at], - current_user: current_user - ) + redirect_url = namespace_project_project_members_path(@project.namespace, @project) - redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.' + if status + redirect_to redirect_url, notice: 'Users were successfully added.' + else + redirect_to redirect_url, alert: 'No users or groups specified.' + end end def update diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 8fea20cefef..953091492ae 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -23,7 +23,7 @@ class Projects::TagsController < Projects::ApplicationController return render_404 unless @tag @release = @project.releases.find_or_initialize_by(tag: @tag.name) - @commit = @repository.commit(@tag.target) + @commit = @repository.commit(@tag.dereferenced_target) end def create diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 76b730198d4..bce5e29d8d8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -30,6 +30,8 @@ class ProjectsController < Projects::ApplicationController @project = ::Projects::CreateService.new(current_user, project_params).execute if @project.saved? + cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) } + redirect_to( project_path(@project), notice: "Project '#{@project.name}' was successfully created." @@ -287,7 +289,8 @@ class ProjectsController < Projects::ApplicationController render 'projects/empty' if @project.empty_repo? else if @project.wiki_enabled? - @wiki_home = @project.wiki.find_page('home', params[:version_id]) + @project_wiki = @project.wiki + @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) @issues = issues_collection @issues = @issues.page(params[:page]) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e27986ef95b..cc2073081b5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -126,7 +126,7 @@ class IssuableFinder @labels = if labels? && !filter_by_no_label? - LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) else Label.none end @@ -273,7 +273,7 @@ class IssuableFinder items = items.with_label(label_names, params[:sort]) if projects - label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id) + label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) items = items.where(labels: { id: label_ids }) end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 44484d64567..865f093f04a 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -4,9 +4,8 @@ class LabelsFinder < UnionFinder @params = params end - def execute(authorized_only: true) - @authorized_only = authorized_only - + def execute(skip_authorization: false) + @skip_authorization = skip_authorization items = find_union(label_ids, Label) items = with_title(items) sort(items) @@ -14,7 +13,7 @@ class LabelsFinder < UnionFinder private - attr_reader :current_user, :params, :authorized_only + attr_reader :current_user, :params, :skip_authorization def label_ids label_ids = [] @@ -70,17 +69,17 @@ class LabelsFinder < UnionFinder end def find_project - if authorized_only - available_projects.find_by(id: project_id) - else + if skip_authorization Project.find_by(id: project_id) + else + available_projects.find_by(id: project_id) end end def projects return @projects if defined?(@projects) - @projects = authorized_only ? available_projects : Project.all + @projects = skip_authorization ? Project.all : available_projects @projects = @projects.in_namespace(group_id) if group_id @projects = @projects.where(id: projects_ids) if projects_ids @projects = @projects.reorder(nil) diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 85e1dc33ee8..dee3c78df47 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -16,13 +16,14 @@ module ButtonHelper # See http://clipboardjs.com/#usage def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' + title = data[:title] || 'Copy to Clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), class: "btn #{css_class}", data: data, type: :button, - title: 'Copy to Clipboard' + title: title end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 03b2db1bc91..ef6cfb235a9 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -71,6 +71,14 @@ module IssuablesHelper author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end + + if issuable.tasks? + output << " ".html_safe + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg") + end + + output end def issuable_todo(issuable) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d26b4018be6..42c00ec3cd5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -174,10 +174,14 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_build, project) + if can?(current_user, :read_pipeline, project) nav_tabs << :pipelines end + if can?(current_user, :read_build, project) + nav_tabs << :builds + end + if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index adda3b8f40c..d3432632899 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -30,23 +30,23 @@ module Ci end event :run do - transition any => :running + transition any - [:running] => :running end event :skip do - transition any => :skipped + transition any - [:skipped] => :skipped end event :drop do - transition any => :failed + transition any - [:failed] => :failed end event :succeed do - transition any => :success + transition any - [:success] => :success end event :cancel do - transition any => :canceled + transition any - [:canceled] => :canceled end # IMPORTANT @@ -260,7 +260,7 @@ module Ci end def update_status - with_lock do + Gitlab::OptimisticLocking.retry_lock(self) do case latest_builds_status when 'pending' then enqueue when 'running' then run diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4cb3a69416e..d159fc6c5c7 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -73,16 +73,16 @@ class CommitStatus < ActiveRecord::Base transition [:created, :pending, :running] => :canceled end - after_transition created: [:pending, :running] do |commit_status| - commit_status.update_attributes queued_at: Time.now + before_transition created: [:pending, :running] do |commit_status| + commit_status.queued_at = Time.now end - after_transition [:created, :pending] => :running do |commit_status| - commit_status.update_attributes started_at: Time.now + before_transition [:created, :pending] => :running do |commit_status| + commit_status.started_at = Time.now end - after_transition any => [:success, :failed, :canceled] do |commit_status| - commit_status.update_attributes finished_at: Time.now + before_transition any => [:success, :failed, :canceled] do |commit_status| + commit_status.finished_at = Time.now end after_transition do |commit_status, transition| diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 17c3b526c97..613444e0d70 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -12,6 +12,7 @@ module Issuable include Subscribable include StripAttribute include Awardable + include Taskable included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 9216122923e..6d88951c713 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -31,7 +31,7 @@ module ProjectFeaturesCompatibility def write_feature_attribute(field, value) build_project_feature unless project_feature - access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED project_feature.update_attribute(field, access_level) end end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 12b23f00769..7edb0acd56c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -38,16 +38,21 @@ module Sortable private - def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: []) + def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) query = Label.select(LabelPriority.arel_table[:priority].minimum). left_join_priorities. joins(:label_links). where("label_priorities.project_id = #{project_column}"). - where(label_links: { target_type: target_type }). where("label_links.target_id = #{target_column}"). reorder(nil) - query.where.not(title: excluded_labels) if excluded_labels.present? + if target_type_column + query = query.where("label_links.target_type = #{target_type_column}") + else + query = query.where(label_links: { target_type: target_type }) + end + + query = query.where.not(title: excluded_labels) if excluded_labels.present? query end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index a3ac577cf3e..ebc75100a54 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -53,10 +53,22 @@ module Taskable # Return a string that describes the current state of this Taskable's task # list items, e.g. "12 of 20 tasks completed" - def task_status + def task_status(short: false) return '' if description.blank? + prep, completed = if short + ['/', ''] + else + [' of ', ' completed'] + end + sum = tasks.summary - "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed" + "#{sum.complete_count}#{prep}#{sum.item_count} #{'task'.pluralize(sum.item_count)}#{completed}" + end + + # Return a short string that describes the current state of this Taskable's + # task list items -- for small screens + def task_status_short + task_status(short: true) end end diff --git a/app/models/event.rb b/app/models/event.rb index 3993b35f96d..43e67069b70 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,6 +1,6 @@ class Event < ActiveRecord::Base include Sortable - default_scope { where.not(author_id: nil) } + default_scope { reorder(nil).where.not(author_id: nil) } CREATED = 1 UPDATED = 2 diff --git a/app/models/group.rb b/app/models/group.rb index 552e1154df6..d9e90cd256a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -68,7 +68,7 @@ class Group < Namespace end def web_url - Gitlab::Routing.url_helpers.group_url(self) + Gitlab::Routing.url_helpers.group_canonical_url(self) end def human_name diff --git a/app/models/group_label.rb b/app/models/group_label.rb index a698b532d19..68841ace2e6 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -5,6 +5,10 @@ class GroupLabel < Label alias_attribute :subject, :group + def subject_foreign_key + 'group_id' + end + def to_reference(source_project = nil, target_project = nil, format: :id) super(source_project, target_project, format: format) end diff --git a/app/models/issue.rb b/app/models/issue.rb index e356fe06363..4f02b02c488 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -5,7 +5,6 @@ class Issue < ActiveRecord::Base include Issuable include Referable include Sortable - include Taskable include Spammable include FasterCacheKeys diff --git a/app/models/label.rb b/app/models/label.rb index 149fd98ecb3..d9287f2dc29 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -92,16 +92,23 @@ class Label < ActiveRecord::Base nil end - def open_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_issues_count(user = nil) + issues_count(user, state: 'opened') end - def closed_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'closed') + def closed_issues_count(user = nil) + issues_count(user, state: 'closed') end - def open_merge_requests_count(user = nil, project = nil) - merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_merge_requests_count(user = nil) + params = { + subject_foreign_key => subject.id, + label_name: title, + scope: 'all', + state: 'opened' + } + + MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count end def prioritize!(project, value) @@ -167,15 +174,8 @@ class Label < ActiveRecord::Base end def issues_count(user, params = {}) - IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count - end - - def merge_requests_count(user, params = {}) - MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count + params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') + IssuesFinder.new(user, params.with_indifferent_access).execute.count end def label_format_reference(format = :id) diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 18657c3e1c8..7712d5783e0 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -17,4 +17,10 @@ class LfsObject < ActiveRecord::Base def project_allowed_access?(project) projects.exists?(storage_project(project).id) end + + def self.destroy_unreferenced + joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id") + .where(lfs_objects_projects: { id: nil }) + .destroy_all + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4872f8b8649..0397c57f935 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base include Issuable include Referable include Sortable - include Taskable include Importable belongs_to :target_project, class_name: "Project" diff --git a/app/models/project.rb b/app/models/project.rb index fbf7012972e..d5512dfaf9c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -30,6 +30,11 @@ class Project < ActiveRecord::Base default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + default_value_for :issues_enabled, gitlab_config_features.issues + default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests + default_value_for :builds_enabled, gitlab_config_features.builds + default_value_for :wiki_enabled, gitlab_config_features.wiki + default_value_for :snippets_enabled, gitlab_config_features.snippets after_create :ensure_dir_exist after_create :create_project_feature, unless: :project_feature @@ -390,7 +395,7 @@ class Project < ActiveRecord::Base end def group_ids - joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id) + joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end end @@ -738,7 +743,7 @@ class Project < ActiveRecord::Base def create_labels Label.templates.each do |label| params = label.attributes.except('id', 'template', 'created_at', 'updated_at') - Labels::FindOrCreateService.new(owner, self, params).execute + Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true) end end diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 33c2b617715..82f47f0e8fd 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -12,6 +12,10 @@ class ProjectLabel < Label alias_attribute :subject, :project + def subject_foreign_key + 'project_id' + end + def to_reference(target_project = nil, format: :id) super(project, target_project, format: format) end diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 81af55aa29a..338e685339a 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,4 +1,6 @@ class BugzillaService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index d9fba3d4a41..b2f426dc2ac 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,4 +1,6 @@ class CustomIssueTrackerService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 5d17c358330..6bd8d4ec568 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,6 +1,8 @@ class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing.url_helpers + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url default_value_for :default, true diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index b26ddd518d7..207bb816ad1 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,6 +1,4 @@ class IssueTrackerService < Service - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? - default_value_for :category, 'issue_tracker' # Pattern used to extract links from comments @@ -38,18 +36,24 @@ class IssueTrackerService < Service ] end - def initialize_properties - if properties.nil? - if enabled_in_gitlab_config + # Initialize with default properties values + # or receive a block with custom properties + def initialize_properties(&block) + return unless properties.nil? + + if enabled_in_gitlab_config + if block_given? + yield + else self.properties = { title: issues_tracker['title'], project_url: issues_tracker['project_url'], issues_url: issues_tracker['issues_url'], new_issue_url: issues_tracker['new_issue_url'] } - else - self.properties = {} end + else + self.properties = {} end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f81b66fd219..5bcf199d468 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,15 +1,32 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null +# + class JiraService < IssueTrackerService - include HTTParty include Gitlab::Routing.url_helpers - DEFAULT_API_VERSION = 2 - - prop_accessor :username, :password, :api_url, :jira_issue_transition_id, - :title, :description, :project_url, :issues_url, :new_issue_url + validates :url, url: true, presence: true, if: :activated? + validates :project_key, presence: true, if: :activated? - validates :api_url, presence: true, url: true, if: :activated? - - before_validation :set_api_url, :set_jira_issue_transition_id + prop_accessor :username, :password, :url, :project_key, + :jira_issue_transition_id, :title, :description before_update :reset_password @@ -18,16 +35,46 @@ class JiraService < IssueTrackerService @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end + def initialize_properties + super do + self.properties = { + title: issues_tracker['title'], + url: issues_tracker['url'] + } + end + end + def reset_password # don't reset the password if a new one is provided - if api_url_changed? && !password_touched? + if url_changed? && !password_touched? self.password = nil end end + def options + url = URI.parse(self.url) + + { + username: self.username, + password: self.password, + site: URI.join(url, '/').to_s, + context_path: url.path, + auth_type: :basic, + read_timeout: 120, + use_ssl: url.scheme == 'https' + } + end + + def client + @client ||= JIRA::Client.new(options) + end + + def jira_project + @jira_project ||= client.Project.find(project_key) + end + def help - 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ - 'allow a user to easily navigate to the Jira issue tracker. See the '\ + 'See the ' \ '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ 'for details.' end @@ -53,12 +100,26 @@ class JiraService < IssueTrackerService end def fields - super.push( - { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + [ + { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' }, + { type: 'text', name: 'project_key', placeholder: 'Project Key' }, { type: 'text', name: 'username', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' }, { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } - ) + ] + end + + # URLs to redirect from Gitlab issues pages to jira issue tracker + def project_url + "#{url}/issues/?jql=project=#{project_key}" + end + + def issues_url + "#{url}/browse/:id" + end + + def new_issue_url + "#{url}/secure/CreateIssue.jspa" end def execute(push, issue = nil) @@ -72,7 +133,7 @@ class JiraService < IssueTrackerService end def create_cross_reference_note(mentioned, noteable, author) - issue_name = mentioned.id + issue_key = mentioned.id project = self.project noteable_name = noteable.class.name.underscore.downcase noteable_id = if noteable.is_a?(Commit) @@ -99,58 +160,28 @@ class JiraService < IssueTrackerService } } - add_comment(data, issue_name) + add_comment(data, issue_key) end def test_settings - return unless api_url.present? - result = JiraService.get( - jira_api_test_url, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) + return unless url.present? + # Test settings by getting the project + jira_project - case result.code - when 201, 200 - Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") - true - else - Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") - false - end - rescue Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + rescue Errno::ECONNREFUSED, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}." false end private - def build_api_url_from_project_url - server = URI(project_url) - default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port]) - server_url = "#{server.scheme}://#{server.host}" - server_url.concat(":#{server.port}") unless default_ports - "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" - rescue - "" # looks like project URL was not valid - end - - def set_api_url - self.api_url = build_api_url_from_project_url if self.api_url.blank? - end - - def set_jira_issue_transition_id - self.jira_issue_transition_id ||= "2" - end - def close_issue(entity, issue) commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) entity.diff_head_sha end + commit_url = build_entity_url(:commit, commit_id) # Depending on the JIRA project's workflow, a comment during transition @@ -161,24 +192,16 @@ class JiraService < IssueTrackerService end def transition_issue(issue) - message = { - transition: { - id: jira_issue_transition_id - } - } - send_message(close_issue_url(issue.iid), message.to_json) + issue = client.Issue.find(issue.iid) + issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end def add_issue_solved_comment(issue, commit_id, commit_url) - comment = { - body: "Issue solved with [#{commit_id}|#{commit_url}]." - } - - send_message(comment_url(issue.iid), comment.to_json) + comment = "Issue solved with [#{commit_id}|#{commit_url}]." + send_message(issue.iid, comment) end - def add_comment(data, issue_name) - url = comment_url(issue_name) + def add_comment(data, issue_key) user_name = data[:user][:name] user_url = data[:user][:url] entity_name = data[:entity][:name] @@ -186,68 +209,31 @@ class JiraService < IssueTrackerService entity_title = data[:entity][:title] project_name = data[:project][:name] - message = { - body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'} - } + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" - unless existing_comment?(issue_name, message[:body]) - send_message(url, message.to_json) + unless comment_exists?(issue_key, message) + send_message(issue_key, message) end end - def auth - require 'base64' - Base64.urlsafe_encode64("#{self.username}:#{self.password}") - end - - def send_message(url, message) - return unless api_url.present? - result = JiraService.post( - url, - body: message, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) - - message = case result.code - when 201, 200, 204 - "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." - when 401 - "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." - else - "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" - end - - Rails.logger.info(message) - message - rescue URI::InvalidURIError, Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." + def comment_exists?(issue_key, message) + comments = client.Issue.find(issue_key).comments + comments.map { |comment| comment.body.include?(message) }.any? end - def existing_comment?(issue_name, new_comment) - return unless api_url.present? - result = JiraService.get( - comment_url(issue_name), - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) + def send_message(issue_key, message) + return unless url.present? - case result.code - when 201, 200 - existing_comments = JSON.parse(result.body)['comments'] + issue = client.Issue.find(issue_key) - if existing_comments.present? - return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any? - end + if issue.comments.build.save!(body: message) + result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." end - false - rescue JSON::ParserError - false + Rails.logger.info(result_message) + result_message + rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" end def resource_url(resource) @@ -267,16 +253,4 @@ class JiraService < IssueTrackerService ) ) end - - def close_issue_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/transitions" - end - - def comment_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/comment" - end - - def jira_api_test_url - "#{self.api_url}/myself" - end end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f634e0772c0..f9da273cf08 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,4 +1,6 @@ class RedmineService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 79d041d2775..a6e911df9bd 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -125,14 +125,8 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user, min_member_access = nil) - member = !!find_member(user.id) - - if min_member_access - member && max_member_access(user.id) >= min_member_access - else - member - end + def member?(user, min_member_access = Gitlab::Access::GUEST) + max_member_access(user.id) >= min_member_access end def human_max_access(user_id) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4ae9c20726f..30be7262438 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -11,6 +11,20 @@ class Repository attr_accessor :path_with_namespace, :project + def self.storages + Gitlab.config.repositories.storages + end + + def self.remove_storage_from_path(repo_path) + storages.find do |_, storage_path| + if repo_path.start_with?(storage_path) + return repo_path.sub(storage_path, '') + end + end + + repo_path + end + def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project @@ -181,7 +195,7 @@ class Repository before_remove_branch branch = find_branch(branch_name) - oldrev = branch.try(:target).try(:id) + oldrev = branch.try(:dereferenced_target).try(:id) newrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name @@ -297,10 +311,10 @@ class Repository # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes number_commits_behind = raw_repository. - count_commits_between(branch.target.sha, root_ref_hash) + count_commits_between(branch.dereferenced_target.sha, root_ref_hash) number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.target.sha) + count_commits_between(root_ref_hash, branch.dereferenced_target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -682,11 +696,11 @@ class Repository branches.sort_by(&:name) when 'updated_desc' branches.sort do |a, b| - commit(b.target).committed_date <=> commit(a.target).committed_date + commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date end when 'updated_asc' branches.sort do |a, b| - commit(a.target).committed_date <=> commit(b.target).committed_date + commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date end else branches @@ -861,7 +875,7 @@ class Repository branch = find_branch(ref) if branch - last_commit = branch.target + last_commit = branch.dereferenced_target index.read_tree(last_commit.raw_commit.tree) parents = [last_commit.sha] end @@ -948,7 +962,7 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -965,7 +979,7 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -994,7 +1008,7 @@ class Repository end def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha args = [commit.id, source_sha] args << { mainline: 1 } if commit.merge_commit? @@ -1008,7 +1022,7 @@ class Repository end def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha args = [commit.id, source_sha] args << 1 if commit.merge_commit? @@ -1081,7 +1095,7 @@ class Repository if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? oldrev = Gitlab::Git::BLANK_SHA else - oldrev = rugged.merge_base(newrev, target_branch.target.sha) + oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) end GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do @@ -1141,7 +1155,7 @@ class Repository end def tags_sorted_by_committed_date - tags.sort_by { |tag| tag.target.committed_date } + tags.sort_by { |tag| tag.dereferenced_target.committed_date } end def keep_around_ref_name(sha) diff --git a/app/models/todo.rb b/app/models/todo.rb index 11c072dd000..f5ade1cc293 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -53,7 +53,7 @@ class Todo < ActiveRecord::Base # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" def order_by_labels_priority params = { - target_type: ['Issue', 'MergeRequest'], + target_type_column: "todos.target_type", target_column: "todos.target_id", project_column: "todos.project_id" } diff --git a/app/models/user.rb b/app/models/user.rb index 9e76df63d31..af3c0b7dc02 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -93,8 +93,10 @@ class User < ActiveRecord::Base # # Validations # + # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true - validates :notification_email, presence: true, email: true + validates :notification_email, presence: true + validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } @@ -256,6 +258,24 @@ class User < ActiveRecord::Base ) end + # searches user by given pattern + # it compares name, email, username fields and user's secondary emails with given pattern + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + + def search_with_secondary_emails(query) + table = arel_table + email_table = Email.arel_table + pattern = "%#{query}%" + matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern)) + + where( + table[:name].matches(pattern). + or(table[:email].matches(pattern)). + or(table[:username].matches(pattern)). + or(table[:id].in(matched_by_emails_user_ids)) + ) + end + def by_login(login) return nil unless login diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index d3dd30b2588..8face432d97 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,17 +10,14 @@ module Ci create_builds! end - @pipeline.with_lock do - new_builds = - stage_indexes_of_created_builds.map do |index| - process_stage(index) - end + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end - @pipeline.update_status + @pipeline.update_status - # Return a flag if a when builds got enqueued - new_builds.flatten.any? - end + new_builds.flatten.any? end private @@ -32,9 +29,11 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) - created_builds_in_stage(index).select do |build| - if HasStatus::COMPLETED_STATUSES.include?(current_status) - process_build(build, current_status) + if HasStatus::COMPLETED_STATUSES.include?(current_status) + created_builds_in_stage(index).select do |build| + Gitlab::OptimisticLocking.retry_lock(build) do |subject| + process_build(subject, current_status) + end end end end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 6973191b203..74b5ebf372b 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -28,17 +28,14 @@ module Ci if build # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachines::InvalidTransition in run! method. - build.with_lock do - build.runner_id = current_runner.id - build.save! - build.run! - end + # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. + build.runner_id = current_runner.id + build.run! end build - rescue StateMachines::InvalidTransition + rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError nil end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 918eddaa53a..3e5dd4ebb86 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -42,7 +42,7 @@ class DeleteBranchService < BaseService Gitlab::DataBuilder::Push.build( project, current_user, - branch.target.sha, + branch.dereferenced_target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index d0cb151a010..d824406cb49 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -36,7 +36,7 @@ class DeleteTagService < BaseService Gitlab::DataBuilder::Push.build( project, current_user, - tag.target.sha, + tag.dereferenced_target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index e6002b03b93..20a4445bddf 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -27,8 +27,8 @@ class GitTagPushService < BaseService tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - if tag && tag.object_sha == params[:newrev] - commit = project.commit(tag.target) + if tag && tag.target == params[:newrev] + commit = project.commit(tag.dereferenced_target) commits = [commit].compact message = tag.message end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 74291312c4e..d622f9edd33 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -2,21 +2,24 @@ module Labels class FindOrCreateService def initialize(current_user, project, params = {}) @current_user = current_user - @group = project.group @project = project @params = params.dup end - def execute + def execute(skip_authorization: false) + @skip_authorization = skip_authorization find_or_create_label end private - attr_reader :current_user, :group, :project, :params + attr_reader :current_user, :project, :params, :skip_authorization def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute + @available_labels ||= LabelsFinder.new( + current_user, + project_id: project.id + ).execute(skip_authorization: skip_authorization) end def find_or_create_label diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index 416aee2ab51..c13f289f61e 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -4,17 +4,25 @@ module Members attr_accessor :source + # source - The source object that respond to `#requesters` (i.g. project or group) + # current_user - The user that performs the access request approval + # params - A hash of parameters + # :user_id - User ID used to retrieve the access requester + # :id - Member ID used to retrieve the access requester + # :access_level - Optional access level set when the request is accepted def initialize(source, current_user, params = {}) @source = source @current_user = current_user - @params = params + @params = params.slice(:user_id, :id, :access_level) end - def execute + # opts - A hash of options + # :force - Bypass permission check: current_user can be nil in that case + def execute(opts = {}) condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } access_requester = source.requesters.find_by!(condition) - raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester) + raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts) access_requester.access_level = params[:access_level] if params[:access_level] access_requester.accept_request @@ -24,8 +32,11 @@ module Members private - def can_update_access_requester?(access_requester) - access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester) + def can_update_access_requester?(access_requester, opts = {}) + access_requester && ( + opts[:force] || + can?(current_user, action_member_permission(:update, access_requester), access_requester) + ) end end end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb new file mode 100644 index 00000000000..e4b24ccef92 --- /dev/null +++ b/app/services/members/create_service.rb @@ -0,0 +1,16 @@ +module Members + class CreateService < BaseService + def execute + return false if params[:user_ids].blank? + + project.team.add_users( + params[:user_ids].split(','), + params[:access_level], + expires_at: params[:expires_at], + current_user: current_user + ) + + true + end + end +end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index e466ffa60eb..d7221fe993c 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -29,7 +29,7 @@ module Projects if unknown_url? # In this case, we only want to import issues, not a repository. create_repository - else + elsif !project.repository_exists? import_repository end end diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 357123c2c13..d3038ae644f 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -10,7 +10,7 @@ = hidden_field_tag "filter", h(params[:filter]) .search-holder .search-field-holder - = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false = icon("search", class: "search-icon") .dropdown - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 2a0302638ba..2411cc45724 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -1,69 +1,70 @@ - page_title "Todos" - header_title "Todos", dashboard_todos_path -.top-area - %ul.nav-links - - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} - = link_to todos_filter_path(state: 'pending') do - %span - To do - %span.badge - = number_with_delimiter(todos_pending_count) - - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} - = link_to todos_filter_path(state: 'done') do - %span - Done - %span.badge - = number_with_delimiter(todos_done_count) +- if current_user.todos.any? + .top-area + %ul.nav-links + - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') + %li{class: "todos-pending #{todo_pending_active}"} + = link_to todos_filter_path(state: 'pending') do + %span + To do + %span.badge + = number_with_delimiter(todos_pending_count) + - todo_done_active = ('active' if params[:state] == 'done') + %li{class: "todos-done #{todo_done_active}"} + = link_to todos_filter_path(state: 'done') do + %span + Done + %span.badge + = number_with_delimiter(todos_done_count) - .nav-controls - - if @todos.any?(&:pending?) - = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do - Mark all as done - = icon('spinner spin') + .nav-controls + - if @todos.any?(&:pending?) + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do + Mark all as done + = icon('spinner spin') -.todos-filters - .row-content-block.second-block - = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do - .filter-item.inline - - if params[:project_id].present? - = hidden_field_tag(:project_id, params[:project_id]) - = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) - .filter-item.inline - - if params[:author_id].present? - = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', - placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) - .filter-item.inline - - if params[:type].present? - = hidden_field_tag(:type, params[:type]) - = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) - .filter-item.inline.actions-filter - - if params[:action_id].present? - = hidden_field_tag(:action_id, params[:action_id]) - = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) - .pull-right - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort - %li - = link_to todos_filter_path(sort: sort_value_priority) do - = sort_title_priority - = link_to todos_filter_path(sort: sort_value_recently_created) do + .todos-filters + .row-content-block.second-block + = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do + .filter-item.inline + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options } }) + .filter-item.inline + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) + .filter-item.inline + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options } }) + .filter-item.inline.actions-filter + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options }}) + .pull-right + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else = sort_title_recently_created - = link_to todos_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %li + = link_to todos_filter_path(sort: sort_value_priority) do + = sort_title_priority + = link_to todos_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to todos_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created .prepend-top-default @@ -78,5 +79,29 @@ %ul.content-list.todos-list = render group[1] = paginate @todos, theme: "gitlab" + - elsif current_user.todos.any? + .todos-all-done + = render "shared/empty_states/todos_all_done.svg" + %h4.text-center + Good job! Looks like you don't have any todos left. + %p.text-center + Are you looking for things to do? Take a look at + = succeed "," do + = link_to "the opened issues", issues_dashboard_path + contribute to + = link_to "merge requests", merge_requests_dashboard_path + or mention someone in a comment to assign a new todo automatically. - else - .nothing-here-block You're all done! + .todos-empty + .todos-empty-hero + = render "shared/empty_states/todos_empty.svg" + .todos-empty-content + %h4 + Todos let you see what you should do next. + %p + When an issue or merge request is assigned to you, or when you + %strong + @mention + in a comment, this will trigger a new item in your todo list, automatically. + %p + You will always know what to work on next. diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 70783a63409..45325d6bc4b 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -13,7 +13,7 @@ .other-labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else .nothing-here-block diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 67f558c854b..3b1295dc3c0 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -7,7 +7,7 @@ = link_to dashboard_todos_path, title: 'Todos' do %span Todos - %span.count= number_with_delimiter(todos_pending_count) + %span.count.js-todos-count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do %span diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4480b2f22c3..9135cee8364 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,4 +1,4 @@ -- commit = @repository.commit(branch.target) +- commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] @@ -31,7 +31,12 @@ = render 'projects/buttons/download', project: @project, ref: branch.name - if can?(current_user, :push_code, @project) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), + class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", + method: :delete, + data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, + remote: true, + "aria-label" => "Delete branch" do = icon("trash-o") - if branch.name != @repository.root_ref diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 55965172d3f..93dca81e6f9 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,14 +1,13 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do - = render_status_with_link('build', 'play') + = ci_icon_for_status('play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span.ci-status-icon - = render_status_with_link('build', subject.status) + = ci_icon_for_status(subject.status) .ci-status-text= subject.name - else %span.ci-status-icon - = render_status_with_link('build', subject.status) - = ci_icon_for_status(subject.status) + = ci_icon_for_status(subject.status) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index c6f359f5679..840f468dc05 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -5,10 +5,7 @@ %tr.commit %td.commit-link = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - - if defined?(status_icon_only) && status_icon_only - = ci_icon_for_status(status) - - else - = ci_status_with_icon(status) + = ci_status_with_icon(status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do @@ -40,7 +37,7 @@ %p.commit-title - if commit = pipeline.commit = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" + = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 6c82a4e5600..d8c95376b94 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,8 +1,23 @@ .commit-info-row.commit-info-row-header - %span.hidden-xs Authored by + %span.hidden-xs.hidden-sm Commit + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace js-details-short" + = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do + %span.text-expander + \... + %span.js-details-content.hide + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm" + = clipboard_button(clipboard_text: @commit.id) + %span.hidden-xs authored + #{time_ago_with_tooltip(@commit.authored_date)} + %span by + = author_avatar(@commit, size: 24) %strong = commit_author_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.authored_date)} + - if @commit.different_committer? + %span.light Committed by + %strong + = commit_committer_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.committed_date)} .pull-right.commit-action-buttons - if defined?(@notes_count) && @notes_count > 0 @@ -33,42 +48,35 @@ %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) -- if @commit.different_committer? - .commit-info-row - %span.light Committed by - %strong - = commit_committer_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.committed_date)} - -.commit-info-row - %span.hidden-xs.hidden-sm Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm" - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline" - = clipboard_button(clipboard_text: @commit.id) - %span.cgray= pluralize(@commit.parents.count, "parent") - - @commit.parents.each do |parent| - = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" - - %span.commit-info.branches - %i.fa.fa-spinner.fa-spin - -- if @commit.status - .commit-info-row - Builds for - = pluralize(@commit.pipelines.count, 'pipeline') - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do - = ci_icon_for_status(@commit.status) - %span.ci-status-label - = ci_label_for_status(@commit.status) - in - = time_interval_in_words @commit.pipelines.total_duration - -.commit-box.content-block +.commit-box %h3.commit-title = markdown(@commit.title, pipeline: :single_line, author: @commit.author) - if @commit.description.present? %pre.commit-description = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) +.commit-info-widget + .widget-row.branch-info + .icon-container.commit-icon + = custom_icon("icon_commit") + %span.cgray= pluralize(@commit.parents.count, "parent") + - @commit.parents.each do |parent| + = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" + %span.commit-info.branches + %i.fa.fa-spinner.fa-spin + + - if @commit.status + .widget-row.pipeline-info + .icon-container + = ci_icon_for_status(@commit.status) + Pipeline + = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" + for + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + %span.ci-status-label + = ci_label_for_status(@commit.status) + in + = time_interval_in_words @commit.pipelines.total_duration + :javascript $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index f2d71fa6989..18daa2ee693 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,7 +1,7 @@ - group_status = CommitStatus.where(id: subject).status %button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span.ci-status-icon - = render_status_with_link('build', group_status) + = ci_icon_for_status(group_status) %span.ci-status-text = name %span.badge= subject.size diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index ac451441eec..2dc91a9b762 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -12,4 +12,4 @@ %th Stages %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, show_commit: false + = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index f9d7eac3542..077b2d2725b 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -3,8 +3,7 @@ = render "projects/commits/head" %div{ class: container_class } - .prepend-top-default - = render "commit_box" + = render "commit_box" = render "ci_menu" = render "builds" diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index d85d6729a81..8233e26e4e7 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -1,7 +1,6 @@ - page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits" -.prepend-top-default - = render "commit_box" += render "commit_box" = render "ci_menu" = render "pipelines_list", pipelines: @ci_pipelines diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index cebf58d63df..b8c64d1f13e 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -4,8 +4,7 @@ = render "projects/commits/head" %div{ class: container_class } - .prepend-top-default - = render "commit_box" + = render "commit_box" - if @commit.status = render "ci_menu" - else diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index b647882efa0..247d612ba6f 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,5 +1,9 @@ - @no_container = true - page_title "Cycle Analytics" + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js') + = render "projects/pipelines/head" #cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 257e0a855bd..8f4f9ad4a80 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -8,7 +8,6 @@ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option') - if editable_diff?(diff_file) - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index a6a2e5690b5..73993f35b39 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -21,6 +21,8 @@ - if diff_file.deleted_file deleted + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard') + - if diff_file.mode_changed? %small = "#{diff_file.a_mode} → #{diff_file.b_mode}" diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 28aad3f4725..78aa9fb7391 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,5 +1,5 @@ / Side-by-side diff view -%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data } +%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - last_line = 0 - diff_file.parallel_diff_lines.each do |line| diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index c45b73e4225..1c457244a7a 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -2,9 +2,9 @@ - if subject.target_url = link_to subject.target_url do %span.ci-status-icon - = render_status_with_link('commit status', subject.status) + = ci_icon_for_status(subject.status) %span.ci-status-text= subject.name - else %span.ci-status-icon - = render_status_with_link('commit status', subject.status) + = ci_icon_for_status(subject.status) %span.ci-status-text= subject.name diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 44683c8bcdb..1892ebb512f 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -4,7 +4,7 @@ %ul.unstyled-list.related-merge-requests - @related_branches.each do |branch| %li - - target = @project.repository.find_branch(branch).target + - target = @project.repository.find_branch(branch).dereferenced_target - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index f135bf6f6b4..05a8475dcd6 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -22,14 +22,14 @@ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? - = render partial: 'shared/label', collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels %ul.content-list.manage-labels-list.js-other-labels - if @labels.present? - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - if @labels.blank? .nothing-here-block diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index bebf0ccd54d..96221a20502 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -5,33 +5,59 @@ %h4.prepend-top-0 = page_title .col-lg-9 - %h5.prepend-top-0 - Pipelines = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| %fieldset.builds-feature - unless @repository.gitlab_ci_yml .form-group %p Pipelines need to be configured before you can begin using Continuous Integration. = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + %hr + .form-group.append-bottom-default + = f.label :runners_token, "Runner token", class: 'label-light' + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used by the Runner to checkout the project + + %hr .form-group - %p Get recent application code using the following command: + %h5.prepend-top-0 + Git strategy for pipelines + %p + Choose between <code>clone</code> or <code>fetch</code> to get the recent application code + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy') .radio = f.label :build_allow_git_fetch_false do = f.radio_button :build_allow_git_fetch, 'false' %strong git clone %br - %span.descr Slower but makes sure you have a clean dir before every build + %span.descr + Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job .radio = f.label :build_allow_git_fetch_true do = f.radio_button :build_allow_git_fetch, 'true' %strong git fetch %br - %span.descr Faster + %span.descr + Faster as it re-uses the project workspace (falling back to clone if it doesn't exist) + %hr .form-group = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' - %p.help-block per build in minutes + %p.help-block + Per job in minutes. If a job passes this threshold, it will be marked as failed. + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout') + + %hr + .form-group + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public pipelines + .help-block + Allow everyone to access pipelines for public and internal projects + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines') + + %hr .form-group = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' .input-group @@ -39,8 +65,9 @@ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' %span.input-group-addon / %p.help-block - We will use this regular expression to find test coverage output in build trace. - Leave blank if you want to disable this feature + A regular expression that will be used to find the test coverage + output in the build trace. Leave blank to disable + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') .bs-callout.bs-callout-info %p Below are examples of regex for existing tools: %ul @@ -57,21 +84,9 @@ gcovr (C/C++) - %code ^TOTAL.*\s+(\d+\%)$ %li - tap --coverage-report=text-summary (Node.js) - + tap --coverage-report=text-summary (NodeJS) - %code ^Statements\s*:\s*([^%]+) - .form-group - .checkbox - = f.label :public_builds do - = f.check_box :public_builds - %strong Public builds - .help-block Allow everyone to access builds traces for Public and Internal projects - - .form-group.append-bottom-default - = f.label :runners_token, "Runners token", class: 'label-light' - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used to checkout project. - = f.submit 'Save changes', class: "btn btn-save" %hr diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 05fccb4f976..c42641afea0 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,4 +1,4 @@ -- commit = @repository.commit(tag.target) +- commit = @repository.commit(tag.dereferenced_target) - release = @releases.find { |release| release.tag == tag.name } %li %div diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 40c8d2af226..6ccdef0df46 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,6 +1,7 @@ - label_css_id = dom_id(label) -- open_issues_count = label.open_issues_count(current_user, @project) -- open_merge_requests_count = label.open_merge_requests_count(current_user, @project) +- open_issues_count = label.open_issues_count(current_user) +- open_merge_requests_count = label.open_merge_requests_count(current_user) +- subject = local_assigns[:subject] %li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label @@ -12,10 +13,10 @@ .dropdown-menu.dropdown-menu-align-right %ul %li - = link_to_label(label, subject: @project, type: :merge_request) do + = link_to_label(label, subject: subject, type: :merge_request) do = pluralize open_merge_requests_count, 'merge request' %li - = link_to_label(label, subject: @project) do + = link_to_label(label, subject: subject) do = pluralize open_issues_count, 'open issue' - if current_user %li.label-subscription{ data: toggle_subscription_data(label) } @@ -28,9 +29,9 @@ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'} .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do = pluralize open_merge_requests_count, 'merge request' - = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do = pluralize open_issues_count, 'open issue' - if current_user diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/_todos_all_done.svg new file mode 100644 index 00000000000..94b5c2e0ea0 --- /dev/null +++ b/app/views/shared/empty_states/_todos_all_done.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg> diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/_todos_empty.svg new file mode 100644 index 00000000000..b1e661268fb --- /dev/null +++ b/app/views/shared/empty_states/_todos_empty.svg @@ -0,0 +1,110 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/> + <mask id="l" width="180" height="220" x="0" y="0" fill="#fff"> + <use xlink:href="#a"/> + </mask> + <rect id="b" width="180" height="220" rx="6"/> + <mask id="m" width="180" height="220" x="0" y="0" fill="#fff"> + <use xlink:href="#b"/> + </mask> + <rect id="c" width="28" height="28" rx="4"/> + <mask id="n" width="28" height="28" x="0" y="0" fill="#fff"> + <use xlink:href="#c"/> + </mask> + <rect id="d" width="28" height="28" rx="4"/> + <mask id="o" width="28" height="28" x="0" y="0" fill="#fff"> + <use xlink:href="#d"/> + </mask> + <circle id="e" cx="21.5" cy="21.5" r="21.5"/> + <mask id="p" width="43" height="43" x="0" y="0" fill="#fff"> + <use xlink:href="#e"/> + </mask> + <circle id="f" cx="26.5" cy="26.5" r="26.5"/> + <mask id="q" width="53" height="53" x="0" y="0" fill="#fff"> + <use xlink:href="#f"/> + </mask> + <circle id="g" cx="9.5" cy="4.5" r="4.5"/> + <mask id="r" width="13" height="13" x="-2" y="-2"> + <path fill="#fff" d="M3-2h13v13H3z"/> + <use xlink:href="#g"/> + </mask> + <circle id="h" cx="26.5" cy="26.5" r="26.5"/> + <mask id="s" width="53" height="53" x="0" y="0" fill="#fff"> + <use xlink:href="#h"/> + </mask> + <circle id="i" cx="21.5" cy="21.5" r="21.5"/> + <mask id="t" width="43" height="43" x="0" y="0" fill="#fff"> + <use xlink:href="#i"/> + </mask> + <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/> + <mask id="u" width="52" height="44" x="0" y="0" fill="#fff"> + <use xlink:href="#j"/> + </mask> + <circle id="k" cx="18.5" cy="18.5" r="18.5"/> + <mask id="v" width="37" height="37" x="0" y="0" fill="#fff"> + <use xlink:href="#k"/> + </mask> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"> + <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/> + <g transform="rotate(5 -707.333 618.042)"> + <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/> + <g transform="translate(29 24)"> + <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> + <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> + <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/> + <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/> + </g> + <g transform="translate(29 69)"> + <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> + <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> + <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/> + <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/> + </g> + <g transform="translate(28 160)"> + <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/> + <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/> + <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/> + </g> + <g transform="translate(28 116)"> + <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/> + <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/> + <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/> + </g> + </g> + <g transform="rotate(-15 601.917 -782.362)"> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/> + <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"> + <tspan x="12" y="27">@</tspan> + </text> + </g> + <g transform="rotate(15 -686.59 1035.907)"> + <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/> + <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/> + <g transform="translate(17 14)"> + <use fill="#FC6D26" xlink:href="#g"/> + <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/> + </g> + </g> + <g transform="rotate(15 -85.125 65.185)"> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/> + <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/> + </g> + <g transform="rotate(-15 716.492 78.873)"> + <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/> + <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/> + </g> + <g transform="rotate(-15 129.114 -585.74)"> + <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/> + <circle cx="16" cy="20" r="2" fill="#FC6D26"/> + <circle cx="27" cy="20" r="2" fill="#FC6D26"/> + <circle cx="38" cy="20" r="2" fill="#FC6D26"/> + </g> + <g transform="rotate(-15 1254.8 -458.986)"> + <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/> + <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/> + <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/> + </g> + </g> +</svg> diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 3c03c220ddd..9e1b0379428 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -3,8 +3,9 @@ - assignee = issuable.assignee - issuable_type = issuable.class.table_name - base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] +- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) -%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) } +%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) } %span - if show_project_name %strong #{project.name} · diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index efb85eafd15..d973e662ff2 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -2,6 +2,8 @@ class ProjectWebHookWorker include Sidekiq::Worker include DedicatedSidekiqQueue + sidekiq_options retry: 4 + def perform(hook_id, data, hook_name) data = data.with_indifferent_access WebHook.find(hook_id).execute(data, hook_name) diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb new file mode 100644 index 00000000000..b80f131d5f7 --- /dev/null +++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb @@ -0,0 +1,8 @@ +class RemoveUnreferencedLfsObjectsWorker + include Sidekiq::Worker + include CronjobQueue + + def perform + LfsObject.destroy_unreferenced + end +end diff --git a/config/application.rb b/config/application.rb index 92c8467e7f4..946b632b0e8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -91,6 +91,7 @@ module Gitlab config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" + config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 74325872b09..c11296975b7 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -101,6 +101,13 @@ :why: GPL-licensed libraries cannot be linked to from non-GPL projects. :versions: [] :when: 2016-05-02 05:29:43.904715000 Z +- - :blacklist + - OSL-3.0 + - :who: Sean McGivern + :why: The OSL license is a copyleft license + :versions: [] + :when: 2016-10-28 11:02:15.540105000 Z + # GEM LICENSES - - :license diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 114ceac8e1f..3451b68cea5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -547,6 +547,10 @@ test: project_url: "http://redmine/projects/:issues_tracker_id" issues_url: "http://redmine/:project_id/:issues_tracker_id/:id" new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" + jira: + title: "JIRA" + url: https://sample_company.atlasian.net + project_key: PROJECT ldap: enabled: false servers: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index efe0ac9c965..9fec2ad6bf7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -307,6 +307,9 @@ Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWork Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *' Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker' +Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *' +Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker' # # GitLab Shell diff --git a/config/mail_room.yml b/config/mail_room.yml index 68697bd1dc4..b026d510f1b 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -27,10 +27,25 @@ :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> :queue: email_receiver :worker: EmailReceiverWorker + <% if config[:sentinels] %> + :sentinels: + <% config[:sentinels].each do |sentinel| %> + - + :host: <%= sentinel[:host] %> + :port: <%= sentinel[:port] %> + <% end %> + <% end %> :arbitration_method: redis :arbitration_options: :redis_url: <%= config[:redis_url].to_json %> :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %> - + <% if config[:sentinels] %> + :sentinels: + <% config[:sentinels].each do |sentinel| %> + - + :host: <%= sentinel[:host] %> + :port: <%= sentinel[:port] %> + <% end %> + <% end %> <% end %> diff --git a/config/routes/group.rb b/config/routes/group.rb index 4838c9d91c6..826048ba196 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -12,23 +12,26 @@ constraints(GroupUrlConstrainer.new) do end end -resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do - member do - get :issues - get :merge_requests - get :projects - get :activity - end - - scope module: :groups do - resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do - post :resend_invite, on: :member - delete :leave, on: :collection +scope constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do + resources :groups, except: [:show] do + member do + get :issues + get :merge_requests + get :projects + get :activity end - resource :avatar, only: [:destroy] - resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + scope module: :groups do + resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do + post :resend_invite, on: :member + delete :leave, on: :collection + end - resources :labels, except: [:show], constraints: { id: /\d+/ } + resource :avatar, only: [:destroy] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + + resources :labels, except: [:show], constraints: { id: /\d+/ } + end end + get 'groups/:id' => 'groups#show', as: :group_canonical end diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb new file mode 100644 index 00000000000..319d86ac159 --- /dev/null +++ b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb @@ -0,0 +1,10 @@ +class RemoveInactiveJiraServiceProperties < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = "Removes all inactive jira_service properties" + + def up + execute("UPDATE services SET properties = '{}' WHERE services.type = 'JiraService' and services.active = false") + end +end diff --git a/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb new file mode 100644 index 00000000000..b47f3aa2810 --- /dev/null +++ b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLockVersionToBuildAndPipelines < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :lock_version, :integer + add_column :ci_commits, :lock_version, :integer + end +end diff --git a/db/migrate/20161025231710_migrate_jira_to_gem.rb b/db/migrate/20161025231710_migrate_jira_to_gem.rb new file mode 100644 index 00000000000..870b00411d2 --- /dev/null +++ b/db/migrate/20161025231710_migrate_jira_to_gem.rb @@ -0,0 +1,73 @@ +class MigrateJiraToGem < ActiveRecord::Migration + DOWNTIME = true + + DOWNTIME_REASON = <<-HEREDOC + Refactor all Jira services properties(serialized field) to use new jira-ruby gem. + There were properties on old Jira service that are not needed anymore after the + service refactoring: api_url, project_url, new_issue_url, issues_url. + We extract the new necessary some properties from old keys and delete them: + taking project_key from project_url and url from api_url + HEREDOC + + def up + active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true" + + select_all(active_services_query).each do |service| + id = service['id'] + properties = JSON.parse(service['properties']) + properties_was = properties.clone + + # Migrate `project_url` to `project_key` + # Ignore if `project_url` doesn't have jql project query with project key + if properties['project_url'].present? + jql = properties['project_url'].match('project=([A-Za-z]*)') + properties['project_key'] = jql.captures.first if jql + end + + # Migrate `api_url` to `url` + if properties['api_url'].present? + url = properties['api_url'].match('(.*)\/rest\/api') + properties['url'] = url.captures.first if url + end + + # Delete now unnecessary properties + properties.delete('api_url') + properties.delete('project_url') + properties.delete('new_issue_url') + properties.delete('issues_url') + + # Update changes properties + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end + + def down + active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true" + + select_all(active_services_query).each do |service| + id = service['id'] + properties = JSON.parse(service['properties']) + properties_was = properties.clone + + # Rebuild old properties based on sane defaults + if properties['url'].present? + properties['api_url'] = "#{properties['url']}/rest/api/2" + properties['project_url'] = + "#{properties['url']}/issues/?jql=project=#{properties['project_key']}" + properties['issues_url'] = "#{properties['url']}/browse/:id" + properties['new_issue_url'] = "#{properties['url']}/secure/CreateIssue.jspa" + end + + # Delete the new properties + properties.delete('url') + properties.delete('project_key') + + # Update changes properties + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 02282b0f666..54b5fc83be0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161024042317) do +ActiveRecord::Schema.define(version: 20161025231710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 20161024042317) do t.text "yaml_variables" t.datetime "queued_at" t.string "token" + t.integer "lock_version" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -219,6 +220,7 @@ ActiveRecord::Schema.define(version: 20161024042317) do t.datetime "finished_at" t.integer "duration" t.integer "user_id" + t.integer "lock_version" end add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree diff --git a/doc/api/README.md b/doc/api/README.md index 3fbe5197a21..f65b934b9db 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -23,6 +23,7 @@ following locations: - [Group Access Requests](access_requests.md) - [Group Members](members.md) - [Issues](issues.md) +- [Issue Boards](boards.md) - [Keys](keys.md) - [Labels](labels.md) - [Merge Requests](merge_requests.md) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index ca44afbf355..5f248ab6f91 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -2,7 +2,7 @@ ## List all deploy keys -Get a list of all deploy keys across all projects. +Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires admin access. ``` GET /deploy_keys diff --git a/doc/api/projects.md b/doc/api/projects.md index b69db90e70d..8ebac57e612 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -598,7 +598,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | @@ -630,7 +630,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | @@ -661,7 +661,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | diff --git a/doc/api/services.md b/doc/api/services.md index 579fdc0c8c9..c7f537aceb6 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -451,43 +451,49 @@ GET /projects/:id/services/irker ## JIRA -Jira issue tracker +JIRA issue tracker. + +### Get JIRA service settings + +Get JIRA service settings for a project. + +``` +GET /projects/:id/services/jira +``` ### Create/Edit JIRA service Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) +>**Note:** +Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to +easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc] +for details. ``` PUT /projects/:id/services/jira ``` -Parameters: - -- `new_issue_url` (**required**) - New Issue url -- `project_url` (**required**) - Project url -- `issues_url` (**required**) - Issue url -- `description` (optional) - Jira issue tracker -- `username` (optional) - Jira username -- `password` (optional) - Jira password +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `active` | boolean| no | Enable/disable the JIRA service. | +| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. | +| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.| +| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` | +| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | +| `description` | string | no | A name for the issue tracker. | +| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | +| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | +| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service -Delete JIRA service for a project. +Remove all previously JIRA settings from a project. ``` DELETE /projects/:id/services/jira ``` -### Get JIRA service settings - -Get JIRA service settings for a project. - -``` -GET /projects/:id/services/jira -``` - ## PivotalTracker Project Management Software (Source Commits Endpoint) @@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project. ``` GET /projects/:id/services/teamcity ``` + +[jira-doc]: ../project_services/jira.md diff --git a/doc/api/tags.md b/doc/api/tags.md index 54059117456..398b080e3f6 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -124,7 +124,7 @@ Parameters: The message will be `nil` when creating a lightweight tag otherwise it will contain the annotation. -It returns 200 if the operation succeed. In case of an error, +It returns 201 if the operation succeed. In case of an error, 405 with an explaining error message is returned. ## Delete a tag diff --git a/doc/ci/README.md b/doc/ci/README.md index 341bc85a16a..6b90940c047 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -19,4 +19,5 @@ - [Build permissions](../user/permissions.md#build-permissions) - [API](../api/ci/README.md) - [CI services (linked docker containers)](services/README.md) +- [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds. diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 79bbe8421c6..a313c31e7ee 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -242,10 +242,10 @@ docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look: - docker push registry.example.com/group/project:latest ``` -You have to use the credentials of the special `gitlab-ci-token` user with its -password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected -to your project. This allows you to automate building and deployment of your -Docker images. +You have to use the special `gitlab-ci-token` user created for you in order to +push to the Registry connected to your project. Its password is provided in the +`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment +of your Docker images. Here's a more elaborate example that splits up the tasks into 4 pipeline stages, including two tests that run in parallel. The build is stored in the container diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 729c1dc8c0d..7d100a4fd93 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -5,9 +5,9 @@ Introduced in GitLab 8.8. ## Pipelines -A pipeline is a group of [builds] that get executed in [stages] \(batches). All -of the builds in a stage are executed in parallel (if there are enough -concurrent [runners]), and if they all succeed, the pipeline moves on to the +A pipeline is a group of [builds][] that get executed in [stages][](batches). +All of the builds in a stage are executed in parallel (if there are enough +concurrent [Runners]), and if they all succeed, the pipeline moves on to the next stage. If one of the builds fails, the next stage is not (usually) executed. @@ -25,8 +25,8 @@ See full [documentation](yaml/README.md#jobs). ## Seeing pipeline status -You can find the current and historical pipeline runs under **Pipelines** for your -project. +You can find the current and historical pipeline runs under **Pipelines** for +your project. ## Seeing build status @@ -36,42 +36,11 @@ cancel the build, retry it, or erase the build trace. ## Badges -There are build status and test coverage report badges available. - -Go to pipeline settings to see available badges and code you can use to embed -badges in the `README.md` or your website. - -### Build status badge - -You can access a build status badge image using following link: - -``` -http://example.gitlab.com/namespace/project/badges/branch/build.svg -``` - -### Test coverage report badge - -GitLab makes it possible to define the regular expression for coverage report, -that each build log will be matched against. This means that each build in the -pipeline can have the test coverage percentage value defined. - -You can access test coverage badge using following link: - -``` -http://example.gitlab.com/namespace/project/badges/branch/coverage.svg -``` - -If you would like to get the coverage report from the specific job, you can add -a `job=coverage_job_name` parameter to the URL. For example, it is possible to -use following Markdown code to embed the est coverage report into `README.md`: - -```markdown -![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage) -``` - -The latest successful pipeline will be used to read the test coverage value. +Build status and test coverage report badges are available. You can find their +respective link in the [Pipelines settings] page. [builds]: #builds [jobs]: yaml/README.md#jobs [stages]: yaml/README.md#stages -[runners]: runners/README.md +[runners]: runners/READM +[pipelines settings]: ../user/project/pipelines/settings.md diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 4cc581dd991..2cfa30f652e 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -93,6 +93,8 @@ merge request. links shift too, which eventually leads to dead links. If you think it is compelling to add numbers in headings, make sure to at least discuss it with someone in the Merge Request +- Avoid adding things that show ephemeral statuses. For example, if a feature is + considered beta or experimental, put this info in a note, not in the heading. - When introducing a new document, be careful for the headings to be grammatically and syntactically correct. It is advised to mention one or all of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`, @@ -466,4 +468,4 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" [ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure" [graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle -[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
\ No newline at end of file +[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 56c8516508e..ece8f880542 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -185,6 +185,20 @@ again in the future. See [the Testing Standards and Style Guidelines](testing.md) for more information. +### Running frontend tests + +`rake teaspoon` runs the frontend-only (JavaScript) tests. +It consists of two subtasks: + +- `rake teaspoon:fixtures` (re-)generates fixtures +- `rake teaspoon:tests` actually executes the tests + +As long as the fixtures don't change, `rake teaspoon:tests` is sufficient +(and saves you some time). + +Please note: Not all of the frontend fixtures are generated. Some are still static +files. These will not be touched by `rake teaspoon:fixtures`. + ## Supported browsers For our currently-supported browsers, see our [requirements][requirements]. @@ -224,13 +238,18 @@ For our currently-supported browsers, see our [requirements][requirements]. [scss-style-guide]: scss_styleguide.md [requirements]: ../install/requirements.md#supported-web-browsers -## Common Errors +## Gotchas -### Rspec (Capybara/Poltergeist) chokes on general JavaScript errors +### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're -working in (`git mv <file>.js> <file.js.es6>`). +working in (`git mv <file.js> <file.js.es6>`). + +Similar errors will be thrown if you're using +any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html) +whether or not you've updated the file extension. + diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 159d5ce286d..b25ce79e89f 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -41,9 +41,9 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9 [Exception]: http://stackoverflow.com/q/10048173/223897 -## Don't use inline CoffeeScript/JavaScript in views +## Don't use inline JavaScript in views -Using the inline `:coffee` or `:coffeescript` Haml filters comes with a +Using the inline `:javascript` Haml filters comes with a performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided. _**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb) @@ -51,9 +51,7 @@ in an initializer._ ### Further reading -- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu) - Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting) -- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897) ## ID-based CSS selectors need to be a bit more specific diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 05972b33fdb..5d177eb26ee 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -62,6 +62,7 @@ Libraries with the following licenses are unacceptable for use: - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. +- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU]. ## Notes @@ -93,3 +94,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/ [GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL [OSI-GPL]: https://opensource.org/faq#linking-proprietary-code +[OSL]: https://opensource.org/licenses/OSL-3.0 +[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL diff --git a/doc/development/testing.md b/doc/development/testing.md index 513457d203a..8e91ac5e3ba 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -36,8 +36,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at `http://localhost:3000/teaspoon` when the Rails server is running. - JavaScript tests live in `spec/javascripts/`, matching the folder structure of - `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding - `spec/javascripts/behaviors/autosize_spec.js.coffee` file. + `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding + `spec/javascripts/behaviors/autosize_spec.js.es6` file. - Haml fixtures required for JavaScript tests live in `spec/javascripts/fixtures`. They should contain the bare minimum amount of markup necessary for the test. diff --git a/doc/integration/README.md b/doc/integration/README.md index c2fd299db07..a928b74f9b8 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png Binary files differindex 0ba737bda9a..0ba737bda9a 100644 --- a/doc/project_services/img/jira_add_user_to_group.png +++ b/doc/integration/img/jira_add_user_to_group.png diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png Binary files differindex 0609060cb05..0609060cb05 100644 --- a/doc/project_services/img/jira_create_new_group.png +++ b/doc/integration/img/jira_create_new_group.png diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png Binary files differindex 53d77b17df0..53d77b17df0 100644 --- a/doc/project_services/img/jira_create_new_group_name.png +++ b/doc/integration/img/jira_create_new_group_name.png diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png Binary files differindex 9eaa444ed25..9eaa444ed25 100644 --- a/doc/project_services/img/jira_create_new_user.png +++ b/doc/integration/img/jira_create_new_user.png diff --git a/doc/project_services/img/jira_group_access.png b/doc/integration/img/jira_group_access.png Binary files differindex 8d4657427ae..8d4657427ae 100644 --- a/doc/project_services/img/jira_group_access.png +++ b/doc/integration/img/jira_group_access.png diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png Binary files differindex 1a2d9f04a6c..1a2d9f04a6c 100644 --- a/doc/project_services/img/jira_issue_reference.png +++ b/doc/integration/img/jira_issue_reference.png diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png Binary files differnew file mode 100644 index 00000000000..b8f6058a514 --- /dev/null +++ b/doc/integration/img/jira_merge_request_close.png diff --git a/doc/project_services/img/jira_project_name.png b/doc/integration/img/jira_project_name.png Binary files differindex e785ec6140d..e785ec6140d 100644 --- a/doc/project_services/img/jira_project_name.png +++ b/doc/integration/img/jira_project_name.png diff --git a/doc/project_services/img/jira_service.png b/doc/integration/img/jira_service.png Binary files differindex 13aefce6f84..13aefce6f84 100644 --- a/doc/project_services/img/jira_service.png +++ b/doc/integration/img/jira_service.png diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png Binary files differindex eed69e80d2c..eed69e80d2c 100644 --- a/doc/project_services/img/jira_service_close_issue.png +++ b/doc/integration/img/jira_service_close_issue.png diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png Binary files differnew file mode 100644 index 00000000000..0cc160bebe2 --- /dev/null +++ b/doc/integration/img/jira_service_page.png diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png Binary files differindex 5f002b59bac..5f002b59bac 100644 --- a/doc/project_services/img/jira_user_management_link.png +++ b/doc/integration/img/jira_user_management_link.png diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png Binary files differindex 937a50a77d9..937a50a77d9 100644 --- a/doc/project_services/img/jira_workflow_screenshot.png +++ b/doc/integration/img/jira_workflow_screenshot.png diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 78aa6634116..cf1557ddc44 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1,197 @@ # GitLab JIRA integration -This document was moved under [project_services/jira](../project_services/jira.md). +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Table of Contents +* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) +* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) +* [Configuration](#configuration) + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` + USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: + ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned JIRA issue. + +For example, for project named `PROJECT` in JIRA, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) + +--- + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's **Services**. + +#### GitLab 13.0 with JIRA v1000.x + +To enable JIRA integration in a project, navigate to the project's +and open the context menu clicking on the top right gear icon, then go to +**Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | +| `Project key` | The short, all capital letter identifier for your JIRA project. | +| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 13.0 and up contain various integration improvements. +We strongly recommend upgrading._ + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://JIRA.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![JIRA services page](img/jira_service.png) + +[services-templates]: ../project_services/services_templates.md +[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index 82505b13401..3f6dfe03d14 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -117,6 +117,22 @@ Click on **Authenticate via U2F Device** to complete the process. This will clear all your two-factor authentication registrations, including mobile applications and U2F devices. +## Personal access tokens + +When 2FA is enabled, you can no longer use your normal account password to +authenticate with Git over HTTPS on the command line, you must use a personal +access token instead. + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Access Tokens**. +1. Choose a name and expiry date for the token. +1. Click on **Create Personal Access Token**. +1. Save the personal access token somewhere safe. + +When using git over HTTPS on the command line, enter the personal access token +into the password field. + ## Note to GitLab administrators You need to take special care to that 2FA keeps working after diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png Binary files differdeleted file mode 100644 index aec472b9118..00000000000 --- a/doc/project_services/img/jira_add_gitlab_commit_message.png +++ /dev/null diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png Binary files differdeleted file mode 100644 index acdd83702d3..00000000000 --- a/doc/project_services/img/jira_issue_closed.png +++ /dev/null diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png Binary files differdeleted file mode 100644 index 0703081d77b..00000000000 --- a/doc/project_services/img/jira_issues_workflow.png +++ /dev/null diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png Binary files differdeleted file mode 100644 index 47785e3ba27..00000000000 --- a/doc/project_services/img/jira_merge_request_close.png +++ /dev/null diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png Binary files differdeleted file mode 100644 index fb270d85e3c..00000000000 --- a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png +++ /dev/null diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png Binary files differdeleted file mode 100644 index a5b49c501ba..00000000000 --- a/doc/project_services/img/jira_service_page.png +++ /dev/null diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png Binary files differdeleted file mode 100644 index 77630d39d39..00000000000 --- a/doc/project_services/img/jira_submit_gitlab_merge_request.png +++ /dev/null diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index b626c746c79..2ea1c58cb31 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,246 +1 @@ -# GitLab JIRA integration - ->**Note:** -Full JIRA integration was previously exclusive to GitLab Enterprise Edition. -With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well. - ---- - -GitLab can be configured to interact with [JIRA Core] either using an -on-premises instance or the SaaS solution that Atlassian offers. Configuration -happens via username and password on a per-project basis. Connecting to a JIRA -server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance or, in -case you have a single JIRA instance, you can pre-fill the JIRA service -settings page in GitLab with a default template. To configure the JIRA template, -see the [Services Templates documentation][services-templates]. - -Once the GitLab project is connected to JIRA, you can reference and close the -issues in JIRA directly from GitLab's merge requests. - -## Configuration - -The configuration consists of two parts: - -- [JIRA configuration](#configuring-jira) -- [GitLab configuration](#configuring-gitlab) - -### Configuring JIRA - -First things first, we need to create a user in JIRA which will have access to -all projects that need to integrate with GitLab. - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![JIRA create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - ->**Note:** -The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required. - ---- - -Assuming you [have already configured JIRA](#configuring-jira), now it's time -to configure GitLab. - -JIRA configuration in GitLab is done via a project's -[**Services**](../project_services/project_services.md). - -To enable JIRA integration in a project, navigate to the project's -**Settings > Services > JIRA**. - -Fill in the required details on the page, as described in the table below. - -| Setting | Description | -| ------- | ----------- | -| `Description` | A name for the issue tracker (to differentiate between instances, for example). | -| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. | -| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | -| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` | -| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | -| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -For example, given the settings below: - -- the JIRA URL is `https://jira.example.com` -- the project is named `GITLAB` -- the user is named `gitlab` -- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) - -the following screenshot shows how the JIRA service settings should look like. - -![JIRA service page](img/jira_service_page.png) - -[trans]: img/jira_issues_workflow.png - ---- - -## JIRA issues - -By now you should have [configured JIRA](#configuring-jira) and enabled the -[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference and close JIRA issues by just mentioning their -ID in GitLab commits and merge requests. - -### Referencing JIRA Issues - -If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link -which points back to JIRA is created. - -The same works for comments in merge requests as well. - -![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) - ---- - -The mentioning action is two-fold, so a comment with a JIRA issue in GitLab -will automatically add a comment in that particular JIRA issue with the link -back to GitLab. - - -![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) - ---- - -The comment on the JIRA issue is of the form: - -> USER mentioned this issue in LINK_TO_THE_MENTION - -Where: - -| Format | Description | -| ------ | ----------- | -| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | -| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | - -### Closing JIRA issues - -JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit which contains the trigger word -followed by the JIRA issue ID in the commit message is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it (provided -the transition ID was set up correctly). - -There are currently three trigger words, and you can use either one to achieve -the same goal: - -- `Resolves GITLAB-1` -- `Closes GITLAB-1` -- `Fixes GITLAB-1` - -where `GITLAB-1` the issue ID of the JIRA project. - -### JIRA issue closing example - -Let's say for example that we submitted a bug fix and created a merge request -in GitLab. The workflow would be something like this: - -1. Create a new branch -1. Fix the bug -1. Commit the changes and push branch to GitLab -1. Open a new merge request and reference the JIRA issue including one of the - trigger words, e.g.: `Fixes GITLAB-1`, in the description -1. Submit the merge request -1. Ask someone to review -1. Merge the merge request -1. The JIRA issue is automatically closed - ---- - -In the following screenshot you can see what the link references to the JIRA -issue look like. - -![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) - ---- - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) - ---- - -You can see from the above image that there are four references to GitLab: - -- The first is from a comment in a specific commit -- The second is from the JIRA issue reference in the merge request description -- The third is from the actual commit that solved the issue -- And the fourth is from the commit that the merge request created - -[services-templates]: ../project_services/services_templates.md "Services templates documentation" -[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" -[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" - -## Troubleshooting - -### GitLab is unable to comment on a ticket - -Make sure that the user you set up for GitLab to communicate with JIRA has the -correct access permission to post comments on a ticket and to also transition the -ticket, if you'd like GitLab to also take care of closing them. - -### GitLab is unable to close a ticket - -Make sure the the `Transition ID` you set within the JIRA settings matches the -one your project needs to close a ticket. +GitLab JIRA integration documentation has moved to [here](../integration/jira.md). diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 4442b7c1742..8116a1ce976 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](jira.md) | JIRA issue tracker | +| [JIRA](../integration/jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png Binary files differindex 57ca2ac5f9e..cd9f5c00eea 100644 --- a/doc/user/project/img/project_settings_list.png +++ b/doc/user/project/img/project_settings_list.png diff --git a/doc/user/project/pipelines/img/pipelines_settings_badges.png b/doc/user/project/pipelines/img/pipelines_settings_badges.png Binary files differnew file mode 100644 index 00000000000..d0c4640791d --- /dev/null +++ b/doc/user/project/pipelines/img/pipelines_settings_badges.png diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png Binary files differnew file mode 100644 index 00000000000..d2a5568521f --- /dev/null +++ b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png Binary files differnew file mode 100644 index 00000000000..3823100daf2 --- /dev/null +++ b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png Binary files differnew file mode 100644 index 00000000000..c4f78803e69 --- /dev/null +++ b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md new file mode 100644 index 00000000000..6cbcf3c400f --- /dev/null +++ b/doc/user/project/pipelines/settings.md @@ -0,0 +1,113 @@ +# CI/CD pipelines settings + +To reach the pipelines settings: + +1. Navigate to your project and click the cog icon in the upper right corner. + + ![Project settings menu](../img/project_settings_list.png) + +1. Select **CI/CD Pipelines** from the menu. + +The following settings can be configured per project. + +## Git strategy + +With Git strategy, you can choose the default way your repository is fetched +from GitLab in a job. + +There are two options: + +- Using `git clone` which is slower since it clones the repository from scratch + for every job, ensuring that the project workspace is always pristine. +- Using `git fetch` which is faster as it re-uses the project workspace (falling + back to clone if it doesn't exist). + +The default Git strategy can be overridden by the [GIT_STRATEGY variable][var] +in `.gitlab-ci.yml`. + +## Timeout + +Timeout defines the maximum amount of time in minutes that a job is able run. +The default value is 60 minutes. Decrease the time limit if you want to impose +a hard limit on your jobs' running time or increase it otherwise. In any case, +if the job surpasses the threshold, it is marked as failed. + +## Test coverage parsing + +If you use test coverage in your code, GitLab can capture its output in the +build log using a regular expression. In the pipelines settings, search for the +"Test coverage parsing" section. + +![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png) + +Leave blank if you want to disable it or enter a ruby regular expression. You +can use http://rubular.com to test your regex. + +If the pipeline succeeds, the coverage is shown in the merge request widget and +in the builds table. + +![MR widget coverage](img/pipelines_test_coverage_mr_widget.png) + +![Build status coverage](img/pipelines_test_coverage_build.png) + +A few examples of known coverage tools for a variety of languages can be found +in the pipelines settings page. + +## Visibility of pipelines + +For public and internal projects, the pipelines page can be accessed by +anyone and those logged in respectively. If you wish to hide it so that only +the members of the project or group have access to it, uncheck the **Public +pipelines** checkbox and save the changes. + +## Badges + +In the pipelines settings page you can find build status and test coverage +badges for your project. The latest successful pipeline will be used to read +the build status and test coverage values. + +Visit the pipelines settings page in your project to see the exact link to +your badges, as well as ways to embed the badge image in your HTML or Markdown +pages. + +![Pipelines badges](img/pipelines_settings_badges.png) + +### Build status badge + +Depending on the status of your build, a badge can have the following values: + +- running +- success +- failed +- skipped +- unknown + +You can access a build status badge image using the following link: + +``` +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg +``` + +### Test coverage report badge + +GitLab makes it possible to define the regular expression for [coverage report], +that each build log will be matched against. This means that each build in the +pipeline can have the test coverage percentage value defined. + +The test coverage badge can be accessed using following link: + +``` +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg +``` + +If you would like to get the coverage report from a specific job, you can add +the `job=coverage_job_name` parameter to the URL. For example, the following +Markdown code will embed the test coverage report badge of the `coverage` job +into your `README.md`: + +```markdown +![coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage) +``` + +[var]: ../../../ci/yaml/README.md#git-strategy +[coverage report]: #test-coverage-parsing diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 244306e8464..007dfb67a77 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see commit ci info' do - expect(page).to have_content "Builds for 1 pipeline pending" + expect(page).to have_content "Pipeline #1 for 570e7b2a pending" end step 'I click status link' do @@ -171,7 +171,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see builds list' do - expect(page).to have_content "Builds for 1 pipeline pending" + expect(page).to have_content "Pipeline #1 for 570e7b2a pending" expect(page).to have_content "1 build" end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 536199ddb4f..bd6466f3686 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -178,17 +178,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I fill jira settings' do - fill_in 'Project url', with: 'http://jira.example' + fill_in 'URL', with: 'http://jira.example' fill_in 'Username', with: 'gitlab' fill_in 'Password', with: 'gitlab' - fill_in 'Api url', with: 'http://jira.example/rest/api/2' + fill_in 'Project Key', with: 'GITLAB' click_button 'Save' end step 'I should see jira service settings saved' do - expect(find_field('Project url').value).to eq 'http://jira.example' + expect(find_field('URL').value).to eq 'http://jira.example' expect(find_field('Username').value).to eq 'gitlab' - expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2' + expect(find_field('Project Key').value).to eq 'GITLAB' end step 'I click Atlassian Bamboo CI service link' do diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 6d827448994..21a106387f0 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -6,58 +6,55 @@ module API before { authenticate! } before { authorize! :download_code, user_project } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get a project repository branches - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/branches + desc 'Get a project repository branches' do + success Entities::RepoBranch + end get ":id/repository/branches" do branches = user_project.repository.branches.sort_by(&:name) present branches, with: Entities::RepoBranch, project: user_project end - # Get a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # GET /projects/:id/repository/branches/:branch - get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do - @branch = user_project.repository.branches.find { |item| item.name == params[:branch] } - not_found!("Branch") unless @branch + desc 'Get a single branch' do + success Entities::RepoBranch + end + params do + requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + end + get ':id/repository/branches/:branch' do + branch = user_project.repository.find_branch(params[:branch]) + not_found!("Branch") unless branch - present @branch, with: Entities::RepoBranch, project: user_project + present branch, with: Entities::RepoBranch, project: user_project end - # Protect a single branch - # # Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}` # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility), # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`. - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # developers_can_push (optional) - Flag if developers can push to that branch - # developers_can_merge (optional) - Flag if developers can merge to that branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/protect - put ':id/repository/branches/:branch/protect', - requirements: { branch: /.+/ } do + desc 'Protect a single branch' do + success Entities::RepoBranch + end + params do + requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch' + optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch' + end + put ':id/repository/branches/:branch/protect' do authorize_admin_project - @branch = user_project.repository.find_branch(params[:branch]) - not_found!('Branch') unless @branch - protected_branch = user_project.protected_branches.find_by(name: @branch.name) + branch = user_project.repository.find_branch(params[:branch]) + not_found!('Branch') unless branch + + protected_branch = user_project.protected_branches.find_by(name: branch.name) protected_branch_params = { - name: @branch.name, - developers_can_push: to_boolean(params[:developers_can_push]), - developers_can_merge: to_boolean(params[:developers_can_merge]) + name: branch.name, + developers_can_push: params[:developers_can_push], + developers_can_merge: params[:developers_can_merge] } service_args = [user_project, current_user, protected_branch_params] @@ -69,39 +66,36 @@ module API end if protected_branch.valid? - present @branch, with: Entities::RepoBranch, project: user_project + present branch, with: Entities::RepoBranch, project: user_project else render_api_error!(protected_branch.errors.full_messages, 422) end end - # Unprotect a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/unprotect - put ':id/repository/branches/:branch/unprotect', - requirements: { branch: /.+/ } do + desc 'Unprotect a single branch' do + success Entities::RepoBranch + end + params do + requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + end + put ':id/repository/branches/:branch/unprotect' do authorize_admin_project - @branch = user_project.repository.find_branch(params[:branch]) - not_found!("Branch") unless @branch - protected_branch = user_project.protected_branches.find_by(name: @branch.name) + branch = user_project.repository.find_branch(params[:branch]) + not_found!("Branch") unless branch + protected_branch = user_project.protected_branches.find_by(name: branch.name) protected_branch.destroy if protected_branch - present @branch, with: Entities::RepoBranch, project: user_project + present branch, with: Entities::RepoBranch, project: user_project end - # Create branch - # - # Parameters: - # id (required) - The ID of a project - # branch_name (required) - The name of the branch - # ref (required) - Create branch from commit sha or existing branch - # Example Request: - # POST /projects/:id/repository/branches + desc 'Create branch' do + success Entities::RepoBranch + end + params do + requires :branch_name, type: String, desc: 'The name of the branch' + requires :ref, type: String, desc: 'Create branch from commit sha or existing branch' + end post ":id/repository/branches" do authorize_push_project result = CreateBranchService.new(user_project, current_user). @@ -116,16 +110,13 @@ module API end end - # Delete branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # DELETE /projects/:id/repository/branches/:branch - delete ":id/repository/branches/:branch", - requirements: { branch: /.+/ } do + desc 'Delete a branch' + params do + requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch' + end + delete ":id/repository/branches/:branch" do authorize_push_project + result = DeleteBranchService.new(user_project, current_user). execute(params[:branch]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index feaa0c213bf..ab9d2d54f4b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -138,7 +138,7 @@ module API expose :name expose :commit do |repo_branch, options| - options[:project].repository.commit(repo_branch.target) + options[:project].repository.commit(repo_branch.dereferenced_target) end expose :protected do |repo_branch, options| @@ -523,7 +523,7 @@ module API expose :name, :message expose :commit do |repo_tag, options| - options[:project].repository.commit(repo_tag.target) + options[:project].repository.commit(repo_tag.dereferenced_target) end expose :release, using: Entities::Release do |repo_tag, options| diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 45120898b76..3c9d7b1aaef 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,17 +1,12 @@ module API module Helpers + include Gitlab::Utils + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def to_boolean(value) - return true if value =~ /^(true|t|yes|y|1|on)$/i - return false if value =~ /^(false|f|no|n|0|off)$/i - - nil - end - def private_token params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9a5d1ece070..ccf181402f9 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -17,15 +17,20 @@ module API # helpers do + def project_path + @project_path ||= begin + project_path = params[:project].sub(/\.git\z/, '') + Repository.remove_storage_from_path(project_path) + end + end + def wiki? - @wiki ||= params[:project].end_with?('.wiki') && - !Project.find_with_namespace(params[:project]) + @wiki ||= project_path.end_with?('.wiki') && + !Project.find_with_namespace(project_path) end def project @project ||= begin - project_path = params[:project] - # Check for *.wiki repositories. # Strip out the .wiki from the pathname before finding the # project. This applies the correct project permissions to diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 9fcd9a3f999..d746070913d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -2,11 +2,14 @@ require 'yaml' module Backup class Repository + def dump prepare Project.find_each(batch_size: 1000) do |project| $progress.print " * #{project.path_with_namespace} ... " + path_to_project_repo = path_to_repo(project) + path_to_project_bundle = path_to_bundle(project) # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace @@ -14,8 +17,22 @@ module Backup if project.empty_repo? $progress.puts "[SKIPPED]".color(:cyan) else - cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) + in_path(path_to_project_repo) do |dir| + FileUtils.mkdir_p(path_to_tars(project)) + cmd = %W(tar -cf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) + output, status = Gitlab::Popen.popen(cmd) + + unless status.zero? + puts "[FAILED]".color(:red) + puts "failed: #{cmd.join(' ')}" + puts output + abort 'Backup failed' + end + end + + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_project_repo} bundle create #{path_to_project_bundle} --all) output, status = Gitlab::Popen.popen(cmd) + if status.zero? $progress.puts "[DONE]".color(:green) else @@ -27,19 +44,22 @@ module Backup end wiki = ProjectWiki.new(project) + path_to_wiki_repo = path_to_repo(wiki) + path_to_wiki_bundle = path_to_bundle(wiki) - if File.exist?(path_to_repo(wiki)) + if File.exist?(path_to_wiki_repo) $progress.print " * #{wiki.path_with_namespace} ... " if wiki.repository.empty? $progress.puts " [SKIPPED]".color(:cyan) else - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? $progress.puts " [DONE]".color(:green) else puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" + puts output abort 'Backup failed' end end @@ -60,40 +80,59 @@ module Backup Project.find_each(batch_size: 1000) do |project| $progress.print " * #{project.path_with_namespace} ... " + path_to_project_repo = path_to_repo(project) + path_to_project_bundle = path_to_bundle(project) project.ensure_dir_exist - if File.exist?(path_to_bundle(project)) - FileUtils.mkdir_p(path_to_repo(project)) - cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)}) + if File.exists?(path_to_project_bundle) + cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo}) else - cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_repo(project)}) + cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo}) end - if system(*cmd, silent) + output, status = Gitlab::Popen.popen(cmd) + if status.zero? $progress.puts "[DONE]".color(:green) else puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" + puts output abort 'Restore failed' end + in_path(path_to_tars(project)) do |dir| + cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) + + output, status = Gitlab::Popen.popen(cmd) + unless status.zero? + puts "[FAILED]".color(:red) + puts "failed: #{cmd.join(' ')}" + puts output + abort 'Restore failed' + end + end + wiki = ProjectWiki.new(project) + path_to_wiki_repo = path_to_repo(wiki) + path_to_wiki_bundle = path_to_bundle(wiki) - if File.exist?(path_to_bundle(wiki)) + if File.exist?(path_to_wiki_bundle) $progress.print " * #{wiki.path_with_namespace} ... " # If a wiki bundle exists, first remove the empty repo # that was initialized with ProjectWiki.new() and then # try to restore with 'git clone --bare'. - FileUtils.rm_rf(path_to_repo(wiki)) - cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) + FileUtils.rm_rf(path_to_wiki_repo) + cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo}) - if system(*cmd, silent) + output, status = Gitlab::Popen.popen(cmd) + if status.zero? $progress.puts " [DONE]".color(:green) else puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" + puts output abort 'Restore failed' end end @@ -101,13 +140,15 @@ module Backup $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args - if system(*cmd) + + output, status = Gitlab::Popen.popen(cmd) + if status.zero? $progress.puts " [DONE]".color(:green) else puts " [FAILED]".color(:red) puts "failed: #{cmd}" + puts output end - end protected @@ -117,11 +158,30 @@ module Backup end def path_to_bundle(project) - File.join(backup_repos_path, project.path_with_namespace + ".bundle") + File.join(backup_repos_path, project.path_with_namespace + '.bundle') + end + + def path_to_tars(project, dir = nil) + path = File.join(backup_repos_path, project.path_with_namespace) + + if dir + File.join(path, "#{dir}.tar") + else + path + end end def backup_repos_path - File.join(Gitlab.config.backup.path, "repositories") + File.join(Gitlab.config.backup.path, 'repositories') + end + + def in_path(path) + return unless Dir.exist?(path) + + dir_entries = Dir.entries(path) + %w[annex custom_hooks].each do |entry| + yield(entry) if dir_entries.include?(entry) + end end def prepare diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index cb213a76a05..3740d4fb4cd 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -102,10 +102,10 @@ module Banzai end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if ref_pattern && link =~ /\A#{ref_pattern}\z/ replace_link_node_with_href(node, link) do - object_link_filter(link, ref_pattern, link_text: text) + object_link_filter(link, ref_pattern, link_content: inner_html) end next @@ -113,9 +113,9 @@ module Banzai next unless link_pattern - if link == text && text =~ /\A#{link_pattern}/ + if link == inner_html && inner_html =~ /\A#{link_pattern}/ replace_link_node_with_text(node, link) do - object_link_filter(text, link_pattern) + object_link_filter(inner_html, link_pattern) end next @@ -123,7 +123,7 @@ module Banzai if link =~ /\A#{link_pattern}\z/ replace_link_node_with_href(node, link) do - object_link_filter(link, link_pattern, link_text: text) + object_link_filter(link, link_pattern, link_content: inner_html) end next @@ -140,11 +140,11 @@ module Banzai # # text - String text to replace references in. # pattern - Reference pattern to match against. - # link_text - Original content of the link being replaced. + # link_content - Original content of the link being replaced. # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text, pattern, link_text: nil) + def object_link_filter(text, pattern, link_content: nil) references_in(text, pattern) do |match, id, project_ref, matches| project = project_from_ref_cached(project_ref) @@ -152,7 +152,7 @@ module Banzai title = object_link_title(object) klass = reference_class(object_sym) - data = data_attributes_for(link_text || match, project, object) + data = data_attributes_for(link_content || match, project, object) if matches.names.include?("url") && matches[:url] url = matches[:url] @@ -160,11 +160,11 @@ module Banzai url = url_for_object_cached(object, project) end - text = link_text || object_link_text(object, matches) + content = link_content || object_link_text(object, matches) %(<a href="#{url}" #{data} title="#{escape_once(title)}" - class="#{klass}">#{escape_once(text)}</a>) + class="#{klass}">#{content}</a>) else match end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 0d20be557a0..dce4de3ceaf 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -37,10 +37,10 @@ module Banzai end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if link =~ ref_start_pattern replace_link_node_with_href(node, link) do - issue_link_filter(link, link_text: text) + issue_link_filter(link, link_content: inner_html) end end end @@ -54,10 +54,11 @@ module Banzai # issue's details page. # # text - String text to replace references in. + # link_content - Original content of the link being replaced. # # Returns a String with `JIRA-123` references replaced with links. All # links have `gfm` and `gfm-issue` class names attached for styling. - def issue_link_filter(text, link_text: nil) + def issue_link_filter(text, link_content: nil) project = context[:project] self.class.references_in(text, issue_reference_pattern) do |match, id| @@ -69,11 +70,11 @@ module Banzai klass = reference_class(:issue) data = data_attribute(project: project.id, external_issue: id) - text = link_text || match + content = link_content || match %(<a href="#{url}" #{data} title="#{escape_once(title)}" - class="#{klass}">#{escape_once(text)}</a>) + class="#{klass}">#{content}</a>) end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index c24831e68ee..9f9a96cdc65 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai end def find_labels(project) - LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false) + LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true) end # Parameters to pass to `Label.find_by` based on the given arguments diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 2d221290f7e..84bfeac8041 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -85,14 +85,14 @@ module Banzai @nodes ||= each_node.to_a end - # Yields the link's URL and text whenever the node is a valid <a> tag. + # Yields the link's URL and inner HTML whenever the node is a valid <a> tag. def yield_valid_link(node) link = CGI.unescape(node.attr('href').to_s) - text = node.text + inner_html = node.inner_html return unless link.force_encoding('UTF-8').valid_encoding? - yield link, text + yield link, inner_html end def replace_text_when_pattern_matches(node, pattern) diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index c6302b586d3..f842b1fb779 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -35,10 +35,10 @@ module Banzai user_link_filter(content) end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if link =~ ref_pattern_start replace_link_node_with_href(node, link) do - user_link_filter(link, link_text: text) + user_link_filter(link, link_content: inner_html) end end end @@ -52,15 +52,16 @@ module Banzai # user's profile page. # # text - String text to replace references in. + # link_content - Original content of the link being replaced. # # Returns a String with `@user` references replaced with links. All links # have `gfm` and `gfm-project_member` class names attached for styling. - def user_link_filter(text, link_text: nil) + def user_link_filter(text, link_content: nil) self.class.references_in(text) do |match, username| if username == 'all' - link_to_all(link_text: link_text) + link_to_all(link_content: link_content) elsif namespace = namespaces[username] - link_to_namespace(namespace, link_text: link_text) || match + link_to_namespace(namespace, link_content: link_content) || match else match end @@ -102,49 +103,49 @@ module Banzai reference_class(:project_member) end - def link_to_all(link_text: nil) + def link_to_all(link_content: nil) project = context[:project] author = context[:author] if author && !project.team.member?(author) - link_text + link_content else url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) data = data_attribute(project: project.id, author: author.try(:id)) - text = link_text || User.reference_prefix + 'all' + content = link_content || User.reference_prefix + 'all' - link_tag(url, data, text, 'All Project and Group Members') + link_tag(url, data, content, 'All Project and Group Members') end end - def link_to_namespace(namespace, link_text: nil) + def link_to_namespace(namespace, link_content: nil) if namespace.is_a?(Group) - link_to_group(namespace.path, namespace, link_text: link_text) + link_to_group(namespace.path, namespace, link_content: link_content) else - link_to_user(namespace.path, namespace, link_text: link_text) + link_to_user(namespace.path, namespace, link_content: link_content) end end - def link_to_group(group, namespace, link_text: nil) + def link_to_group(group, namespace, link_content: nil) url = urls.group_url(group, only_path: context[:only_path]) data = data_attribute(group: namespace.id) - text = link_text || Group.reference_prefix + group + content = link_content || Group.reference_prefix + group - link_tag(url, data, text, namespace.name) + link_tag(url, data, content, namespace.name) end - def link_to_user(user, namespace, link_text: nil) + def link_to_user(user, namespace, link_content: nil) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) - text = link_text || User.reference_prefix + user + content = link_content || User.reference_prefix + user - link_tag(url, data, text, namespace.owner_name) + link_tag(url, data, content, namespace.owner_name) end - def link_tag(url, data, text, title) - %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>) + def link_tag(url, data, link_content, title) + %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) end end end diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb index 0df3a72d1c4..de3ebe72720 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/redactor.rb @@ -41,10 +41,10 @@ module Banzai next if visible.include?(node) doc_data[:visible_reference_count] -= 1 - # The reference should be replaced by the original text, - # which is not always the same as the rendered text. - text = node.attr('data-original') || node.text - node.replace(text) + # The reference should be replaced by the original link's content, + # which is not always the same as the rendered one. + content = node.attr('data-original') || node.inner_html + node.replace(content) end end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 4f81863da35..d76aa38f741 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -83,7 +83,7 @@ module Gitlab tag = repository.find_tag(tag_name) if tag - commit = repository.commit(tag.target) + commit = repository.commit(tag.dereferenced_target) commit.try(:sha) end else diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 65ee85ca5a9..222bcdcbf9c 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -75,7 +75,7 @@ module Gitlab def create_label(name) params = { title: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def user_info(person_id) @@ -133,7 +133,7 @@ module Gitlab updated_at: DateTime.parse(bug['dtLastUpdated']) ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 8cacf4f4925..6dbae64a9fe 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -10,7 +10,9 @@ module Gitlab end def create! - self.klass.create!(self.attributes) + project.public_send(project_association).find_or_create_by!(find_condition) do |record| + record.attributes = attributes + end end private diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 7f424b74efb..85df6547a67 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -105,18 +105,20 @@ module Gitlab data = api.send(method, *args) return data unless data.is_a?(Array) + last_response = api.last_response + if block_given? yield data - each_response_page(&block) + # api.last_response could change while we're yielding (e.g. fetching labels for each PR) + # so we cache our own last response + each_response_page(last_response, &block) else - each_response_page { |page| data.concat(page) } + each_response_page(last_response) { |page| data.concat(page) } data end end - def each_response_page - last_response = api.last_response - + def each_response_page(last_response) while last_response.rels[:next] sleep rate_limit_sleep_time if rate_limit_exceed? last_response = last_response.rels[:next].get diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 4b70f33a851..ecc28799737 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -24,7 +24,8 @@ module Gitlab import_milestones import_issues import_pull_requests - import_comments + import_comments(:issues) + import_comments(:pull_requests) import_wiki import_releases handle_errors @@ -48,7 +49,7 @@ module Gitlab end def import_labels - client.labels(repo, per_page: 100) do |labels| + fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin label = LabelFormatter.new(project, raw).create! @@ -61,7 +62,7 @@ module Gitlab end def import_milestones - client.milestones(repo, state: :all, per_page: 100) do |milestones| + fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| milestones.each do |raw| begin MilestoneFormatter.new(project, raw).create! @@ -73,7 +74,7 @@ module Gitlab end def import_issues - client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| + fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| gh_issue = IssueFormatter.new(project, raw) @@ -90,7 +91,7 @@ module Gitlab end def import_pull_requests - client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| + fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| pull_request = PullRequestFormatter.new(project, raw) next unless pull_request.valid? @@ -132,8 +133,15 @@ module Gitlab end def apply_labels(issuable, raw_issuable) - if raw_issuable.labels.count > 0 - label_ids = raw_issuable.labels + # GH returns labels for issues but not for pull requests! + labels = if issuable.is_a?(MergeRequest) + client.labels_for_issue(repo, raw_issuable.number) + else + raw_issuable.labels + end + + if labels.count > 0 + label_ids = labels .map { |attrs| @labels[attrs.name] } .compact @@ -141,23 +149,35 @@ module Gitlab end end - def import_comments - client.issues_comments(repo, per_page: 100) do |comments| - create_comments(comments, :issue) - end + def import_comments(issuable_type) + resource_type = "#{issuable_type}_comments".to_sym + + # Two notes here: + # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note, + # compare it against every comment in the current imported page until we find match, and that's where start importing + # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns + # only comments on diffs, so select last note not based on noteable_type but on line_code + line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL' + last_note = project.notes.where("line_code IS #{line_code_is}").last + + fetch_resources(resource_type, repo, per_page: 100) do |comments| + if last_note + discard_inserted_comments(comments, last_note) + last_note = nil + end - client.pull_requests_comments(repo, per_page: 100) do |comments| - create_comments(comments, :pull_request) + create_comments(comments) end end - def create_comments(comments, issuable_type) + def create_comments(comments) ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) - issuable_class = issuable_type == :issue ? Issue : MergeRequest - iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly + comment = CommentFormatter.new(project, raw) + # GH does not return info about comment's parent, so we guess it by checking its URL! + *_, parent, iid = URI(raw.html_url).path.split('/') + issuable_class = parent == 'issues' ? Issue : MergeRequest issuable = issuable_class.find_by_iid(iid) next unless issuable @@ -169,6 +189,24 @@ module Gitlab end end + def discard_inserted_comments(comments, last_note) + last_note_attrs = nil + + cut_off_index = comments.find_index do |raw| + comment = CommentFormatter.new(project, raw) + comment_attrs = comment.attributes + last_note_attrs ||= last_note.slice(*comment_attrs.keys) + + comment_attrs.with_indifferent_access == last_note_attrs + end + + # No matching resource in the collection, which means we got halted right on the end of the last page, so all good + return unless cut_off_index + + # Otherwise, remove the resources we've already inserted + comments.shift(cut_off_index + 1) + end + def import_wiki unless project.wiki.repository_exists? wiki = WikiFormatter.new(project) @@ -184,7 +222,7 @@ module Gitlab end def import_releases - client.releases(repo, per_page: 100) do |releases| + fetch_resources(:releases, repo, per_page: 100) do |releases| releases.each do |raw| begin gh_release = ReleaseFormatter.new(project, raw) @@ -195,6 +233,47 @@ module Gitlab end end end + + def fetch_resources(resource_type, *opts) + return if imported?(resource_type) + + opts.last.merge!(page: current_page(resource_type)) + + client.public_send(resource_type, *opts) do |resources| + yield resources + increment_page(resource_type) + end + + imported!(resource_type) + end + + def imported?(resource_type) + Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported") + end + + def imported!(resource_type) + Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day) + end + + def increment_page(resource_type) + key = "#{cache_key_prefix}:#{resource_type}:current-page" + + # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is + # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around + page = Rails.cache.read(key) + page += 1 + Rails.cache.write(key, page) + + page + end + + def current_page(resource_type) + Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 } + end + + def cache_key_prefix + @cache_key_prefix ||= "github-import:#{project.id}" + end end end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 77621de9f4c..8c32ac59fc5 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -20,8 +20,12 @@ module Gitlab raw_data.comments > 0 end - def klass - Issue + def project_association + :issues + end + + def find_condition + { iid: number } end def number diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 942dfb3312b..211ccdc51bb 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -9,14 +9,14 @@ module Gitlab } end - def klass - Label + def project_association + :labels end def create! params = attributes.except(:project) - service = ::Labels::FindOrCreateService.new(project.owner, project, params) - label = service.execute + service = ::Labels::FindOrCreateService.new(nil, project, params) + label = service.execute(skip_authorization: true) raise ActiveRecord::RecordInvalid.new(label) unless label.persisted? diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index b2fa524cf5b..401dd962521 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -14,8 +14,12 @@ module Gitlab } end - def klass - Milestone + def project_association + :milestones + end + + def find_condition + { iid: raw_data.number } end private diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 1408683100f..b9a227fb11a 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -24,8 +24,12 @@ module Gitlab } end - def klass - MergeRequest + def project_association + :merge_requests + end + + def find_condition + { iid: number } end def number diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb index 73d643b00ad..1ad702a6058 100644 --- a/lib/gitlab/github_import/release_formatter.rb +++ b/lib/gitlab/github_import/release_formatter.rb @@ -11,8 +11,12 @@ module Gitlab } end - def klass - Release + def project_association + :releases + end + + def find_condition + { tag: raw_data.tag_name } end def valid? diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 6a68e786b4f..1f4edc36928 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -101,7 +101,7 @@ module Gitlab state: raw_issue['state'] == 'closed' ? 'closed' : 'opened' ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) @@ -235,7 +235,7 @@ module Gitlab def create_label(name) params = { name: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def format_content(raw_content) diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index dbc759367eb..b8ca7f2f55f 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -19,7 +19,7 @@ module Gitlab ] labels.each do |params| - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end end end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 12999a90a29..a5220d92312 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -33,7 +33,12 @@ module Gitlab config[:mailbox] = 'inbox' if config[:mailbox].nil? if config[:enabled] && config[:address] - config[:redis_url] = Gitlab::Redis.new(rails_env).url + gitlab_redis = Gitlab::Redis.new(rails_env) + config[:redis_url] = gitlab_redis.url + + if gitlab_redis.sentinels? + config[:sentinels] = gitlab_redis.sentinels + end end config diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb new file mode 100644 index 00000000000..879d46446b3 --- /dev/null +++ b/lib/gitlab/optimistic_locking.rb @@ -0,0 +1,19 @@ +module Gitlab + module OptimisticLocking + extend self + + def retry_lock(subject, retries = 100, &block) + loop do + begin + ActiveRecord::Base.transaction do + return block.call(subject) + end + rescue ActiveRecord::StaleObjectError + retries -= 1 + raise unless retries >= 0 + subject.reload + end + end + end + end +end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index c649da8c426..9226da2d6b1 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -63,6 +63,14 @@ module Gitlab raw_config_hash[:url] end + def sentinels + raw_config_hash[:sentinels] + end + + def sentinels? + sentinels && !sentinels.empty? + end + private def redis_store_options diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index e59ead5d76c..4c395b4266e 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -13,5 +13,13 @@ module Gitlab def force_utf8(str) str.force_encoding(Encoding::UTF_8) end + + def to_boolean(value) + return value if [true, false].include?(value) + return true if value =~ /^(true|t|yes|y|1|on)$/i + return false if value =~ /^(false|f|no|n|0|off)$/i + + nil + end end end diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake new file mode 100644 index 00000000000..d43cbad1909 --- /dev/null +++ b/lib/tasks/eslint.rake @@ -0,0 +1,7 @@ +unless Rails.env.production? + desc "GitLab | Run ESLint" + task :eslint do + system("npm", "run", "eslint") + end +end + diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake new file mode 100644 index 00000000000..32b668df3bf --- /dev/null +++ b/lib/tasks/lint.rake @@ -0,0 +1,9 @@ +unless Rails.env.production? + namespace :lint do + desc "GitLab | lint | Lint JavaScript files using ESLint" + task :javascript do + Rake::Task['eslint'].invoke + end + end +end + diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake new file mode 100644 index 00000000000..08caedd7ff3 --- /dev/null +++ b/lib/tasks/teaspoon.rake @@ -0,0 +1,25 @@ +unless Rails.env.production? + Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon') + + namespace :teaspoon do + desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests' + RSpec::Core::RakeTask.new(:fixtures) do |t| + ENV['NO_KNAPSACK'] = 'true' + t.pattern = 'spec/javascripts/fixtures/*.rb' + t.rspec_opts = '--format documentation' + end + + desc 'GitLab | Teaspoon | Run JavaScript tests' + task :tests do + require "teaspoon/console" + options = {} + abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures? + end + end + + desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests' + task :teaspoon do + Rake::Task['teaspoon:fixtures'].invoke + Rake::Task['teaspoon:tests'].invoke + end +end diff --git a/package.json b/package.json new file mode 100644 index 00000000000..d440307bd10 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "scripts": { + "eslint": "eslint --ext .js,.js.es6 .", + "eslint-fix": "eslint --fix --ext .js,.js.es6 ." + }, + "devDependencies": { + "eslint": "^3.1.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^2.0.1", + "eslint-plugin-jsx-a11y": "^2.2.3", + "eslint-plugin-react": "^6.4.1" + } +} diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index c5d3cd70acc..22bf3055538 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe 'mail_room.yml' do let(:config_path) { 'config/mail_room.yml' } let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) } + before(:each) { clear_raw_config } + after(:each) { clear_raw_config } context 'when incoming email is disabled' do before do @@ -20,6 +22,9 @@ describe 'mail_room.yml' do end context 'when incoming email is enabled' do + let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) } + before do ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s Gitlab::MailRoom.reset_config! @@ -30,8 +35,9 @@ describe 'mail_room.yml' do end it 'contains the intended configuration' do - expect(configuration[:mailboxes].length).to eq(1) + stub_const('Gitlab::Redis::CONFIG_FILE', redis_config) + expect(configuration[:mailboxes].length).to eq(1) mailbox = configuration[:mailboxes].first expect(mailbox[:host]).to eq('imap.gmail.com') @@ -42,10 +48,26 @@ describe 'mail_room.yml' do expect(mailbox[:password]).to eq('[REDACTED]') expect(mailbox[:name]).to eq('inbox') - redis_url = Gitlab::Redis.url + redis_url = gitlab_redis.url + sentinels = gitlab_redis.sentinels + expect(mailbox[:delivery_options][:redis_url]).to be_present expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) + + expect(mailbox[:delivery_options][:sentinels]).to be_present + expect(mailbox[:delivery_options][:sentinels]).to eq(sentinels) + + expect(mailbox[:arbitration_options][:redis_url]).to be_present expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url) + + expect(mailbox[:arbitration_options][:sentinels]).to be_present + expect(mailbox[:arbitration_options][:sentinels]).to eq(sentinels) end end + + def clear_raw_config + Gitlab::Redis.remove_instance_variable(:@_raw_config) + rescue NameError + # raised if @_raw_config was not set; ignore + end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 33fe3c73822..2ab2ca1b667 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -114,6 +114,17 @@ describe Admin::UsersController do end end + describe 'POST create' do + it 'creates the user' do + expect{ post :create, user: attributes_for(:user) }.to change{ User.count }.by(1) + end + + it 'shows only one error message for an invalid email' do + post :create, user: attributes_for(:user, email: 'bogus') + expect(assigns[:user].errors).to contain_exactly("Email is invalid") + end + end + describe 'POST update' do context 'when the password has changed' do def update_password(user, password, password_confirmation = nil) diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 41df63d445a..8faecec0063 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::LabelsController do let(:group) { create(:group) } - let(:project) { create(:project, namespace: group) } + let(:project) { create(:empty_project, namespace: group) } let(:user) { create(:user) } before do @@ -73,16 +73,27 @@ describe Projects::LabelsController do describe 'POST #generate' do let(:admin) { create(:admin) } - let(:project) { create(:empty_project) } before do sign_in(admin) end - it 'creates labels' do - post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + context 'personal project' do + let(:personal_project) { create(:empty_project) } - expect(response).to have_http_status(302) + it 'creates labels' do + post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param + + expect(response).to have_http_status(302) + end + end + + context 'project belonging to a group' do + it 'creates labels' do + post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to have_http_status(302) + end end end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 4e3ef5dc6fa..7c5f33c63b8 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -20,7 +20,7 @@ describe Projects::MilestonesController do delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js expect(response).to be_success - expect(Event.first.action).to eq(Event::DESTROYED) + expect(Event.recent.first.action).to eq(Event::DESTROYED) expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound) issue.reload diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index b4f066d8600..2a7523c6512 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -14,49 +14,49 @@ describe Projects::ProjectMembersController do end describe 'POST create' do - context 'when users are added' do - let(:project_user) { create(:user) } + let(:project_user) { create(:user) } - before { sign_in(user) } + before { sign_in(user) } - context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + context 'when user does not have enough rights' do + before { project.team << [user, :developer] } - it 'returns 404' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: project_user.id, - access_level: Gitlab::Access::GUEST + it 'returns 404' do + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST - expect(response).to have_http_status(404) - expect(project.users).not_to include project_user - end + expect(response).to have_http_status(404) + expect(project.users).not_to include project_user end + end - context 'when user has enough rights' do - before { project.team << [user, :master] } + context 'when user has enough rights' do + before { project.team << [user, :master] } - it 'adds user to members' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: project_user.id, - access_level: Gitlab::Access::GUEST - - expect(response).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) - expect(project.users).to include project_user - end + it 'adds user to members' do + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true) - it 'adds no user to members' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: '', - access_level: Gitlab::Access::GUEST + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST - expect(response).to set_flash.to 'No users or groups specified.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) - expect(project.users).not_to include project_user - end + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + end + + it 'adds no user to members' do + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false) + + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: '', + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'No users or groups specified.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 4065e2defbc..dd4a86b1e31 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -118,10 +118,9 @@ FactoryGirl.define do project.create_jira_service( active: true, properties: { - 'title' => 'JIRA tracker', - 'project_url' => 'http://jira.example/issues/?jql=project=A', - 'issues_url' => 'http://jira.example/browse/:id', - 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa' + title: 'JIRA tracker', + url: 'http://jira.example.net', + project_key: 'JIRA' } ) end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index c533ce1d87f..3234fabe288 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -347,6 +347,19 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.board', count: 5) end + it 'keeps dropdown open after adding new list' do + click_button 'Create new list' + wait_for_ajax + + page.within('.dropdown-menu-issues-board-new') do + click_link done.title + end + + wait_for_vue_resource + + expect(find('.issue-boards-search')).to have_selector('.open') + end + it 'moves issues from backlog into new list' do wait_for_board_cards(1, 6) diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index c6adf7e4c56..5e6d8467217 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -194,12 +194,12 @@ feature 'Diff notes resolve', feature: true, js: true do context 'multiple notes' do before do create(:diff_note_on_merge_request, project: project, noteable: merge_request) + visit_merge_request end it 'does not mark discussion as resolved when resolving single note' do - page.within '.diff-content .note' do + page.first '.diff-content .note' do first('.line-resolve-btn').click - sleep 1 expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end @@ -212,7 +212,9 @@ feature 'Diff notes resolve', feature: true, js: true do it 'resolves discussion' do page.all('.note').each do |note| - note.find('.line-resolve-btn').click + note.all('.line-resolve-btn').each do |button| + button.click + end end expect(page).to have_content('Resolved by') @@ -292,7 +294,7 @@ feature 'Diff notes resolve', feature: true, js: true do expect(holder).to have_selector('.discussion-next-btn') end end - + it 'displays next discussion even if hidden' do page.all('.note-discussion').each do |discussion| page.within discussion do diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb new file mode 100644 index 00000000000..8b603f51545 --- /dev/null +++ b/spec/features/milestones/milestones_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +describe 'Milestone draggable', feature: true, js: true do + let(:milestone) { create(:milestone, project: project, title: 8.14) } + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + context 'issues' do + let(:issue) { page.find_by_id('issues-list-unassigned').find('li') } + let(:issue_target) { page.find_by_id('issues-list-ongoing') } + + it 'does not allow guest to drag issue' do + create_and_drag_issue + + expect(issue_target).not_to have_selector('.issuable-row') + end + + it 'does not allow authorized user to drag issue' do + login_as(user) + create_and_drag_issue + + expect(issue_target).not_to have_selector('.issuable-row') + end + + it 'allows author to drag issue' do + login_as(user) + create_and_drag_issue(author: user) + + expect(issue_target).to have_selector('.issuable-row') + end + + it 'allows admin to drag issue' do + login_as(:admin) + create_and_drag_issue + + expect(issue_target).to have_selector('.issuable-row') + end + end + + context 'merge requests' do + let(:merge_request) { page.find_by_id('merge_requests-list-unassigned').find('li') } + let(:merge_request_target) { page.find_by_id('merge_requests-list-ongoing') } + + it 'does not allow guest to drag merge request' do + create_and_drag_merge_request + + expect(merge_request_target).not_to have_selector('.issuable-row') + end + + it 'does not allow authorized user to drag merge request' do + login_as(user) + create_and_drag_merge_request + + expect(merge_request_target).not_to have_selector('.issuable-row') + end + + it 'allows author to drag merge request' do + login_as(user) + create_and_drag_merge_request(author: user) + + expect(merge_request_target).to have_selector('.issuable-row') + end + + it 'allows admin to drag merge request' do + login_as(:admin) + create_and_drag_merge_request + + expect(merge_request_target).to have_selector('.issuable-row') + end + end + + def create_and_drag_issue(params = {}) + create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone)) + + visit namespace_project_milestone_path(project.namespace, project, milestone) + issue.drag_to(issue_target) + end + + def create_and_drag_merge_request(params = {}) + create(:merge_request, params.merge(title: 'Foo', source_project: project, target_project: project, milestone: milestone)) + + visit namespace_project_milestone_path(project.namespace, project, milestone) + page.find("a[href='#tab-merge-requests']").click + merge_request.drag_to(merge_request_target) + end +end diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb deleted file mode 100644 index 63878c55421..00000000000 --- a/spec/features/projects/branches/delete_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -feature 'Delete branch', feature: true, js: true do - include WaitForAjax - - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.team << [user, :master] - login_as user - visit namespace_project_branches_path(project.namespace, project) - end - - it 'destroys tooltip' do - first('.remove-row').hover - expect(page).to have_selector('.tooltip') - - first('.remove-row').click - wait_for_ajax - - expect(page).not_to have_selector('.tooltip') - end -end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 1d4484a9edd..d25cf7fb353 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -41,6 +41,22 @@ describe 'Edit Project Settings', feature: true do end end end + + context "pipelines subtabs" do + it "shows builds when enabled" do + visit namespace_project_pipelines_path(project.namespace, project) + + expect(page).to have_selector(".shortcuts-builds") + end + + it "hides builds when disabled" do + allow(Ability).to receive(:allowed?).with(member, :read_builds, project).and_return(false) + + visit namespace_project_pipelines_path(project.namespace, project) + + expect(page).not_to have_selector(".shortcuts-builds") + end + end end describe 'project features visibility pages' do diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb new file mode 100644 index 00000000000..b4f5f6b3fc5 --- /dev/null +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Projects > Wiki > User views wiki in project page', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'when repository is disabled for project' do + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + end + + context 'when wiki homepage contains a link' do + before do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + it 'displays the correct URL for the link' do + visit namespace_project_path(project.namespace, project) + expect(page).to have_link( + 'some link', + href: namespace_project_wiki_path( + project.namespace, + project, + 'other-page' + ) + ) + end + end + end +end diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb index e74a51acede..fec28c55d30 100644 --- a/spec/features/todos/todos_sorting_spec.rb +++ b/spec/features/todos/todos_sorting_spec.rb @@ -8,60 +8,90 @@ describe "Dashboard > User sorts todos", feature: true do let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } - let(:issue_1) { create(:issue, title: 'issue_1', project: project) } - let(:issue_2) { create(:issue, title: 'issue_2', project: project) } - let(:issue_3) { create(:issue, title: 'issue_3', project: project) } - let(:issue_4) { create(:issue, title: 'issue_4', project: project) } - - let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } - - before do - create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) - create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago) - create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago) - create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago) - create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) - - merge_request_1.labels << label_1 - issue_3.labels << label_1 - issue_2.labels << label_3 - issue_1.labels << label_2 - - project.team << [user, :developer] - login_as(user) - visit dashboard_todos_path - end + before { project.team << [user, :developer] } - it "sorts with oldest created todos first" do - click_link "Last created" + context 'sort options' do + let(:issue_1) { create(:issue, title: 'issue_1', project: project) } + let(:issue_2) { create(:issue, title: 'issue_2', project: project) } + let(:issue_3) { create(:issue, title: 'issue_3', project: project) } + let(:issue_4) { create(:issue, title: 'issue_4', project: project) } - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("merge_request_1") - expect(results_list.all('p')[1]).to have_content("issue_1") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") - end + let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") } + + before do + create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago) + create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago) + create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago) + create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago) + create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + + merge_request_1.labels << label_1 + issue_3.labels << label_1 + issue_2.labels << label_3 + issue_1.labels << label_2 + + login_as(user) + visit dashboard_todos_path + end + + it "sorts with oldest created todos first" do + click_link "Last created" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("merge_request_1") + expect(results_list.all('p')[1]).to have_content("issue_1") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end - it "sorts with newest created todos first" do - click_link "Oldest created" + it "sorts with newest created todos first" do + click_link "Oldest created" - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_4") - expect(results_list.all('p')[1]).to have_content("issue_2") - expect(results_list.all('p')[2]).to have_content("issue_3") - expect(results_list.all('p')[3]).to have_content("issue_1") - expect(results_list.all('p')[4]).to have_content("merge_request_1") + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_4") + expect(results_list.all('p')[1]).to have_content("issue_2") + expect(results_list.all('p')[2]).to have_content("issue_3") + expect(results_list.all('p')[3]).to have_content("issue_1") + expect(results_list.all('p')[4]).to have_content("merge_request_1") + end + + it "sorts by priority" do + click_link "Priority" + + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_3") + expect(results_list.all('p')[1]).to have_content("merge_request_1") + expect(results_list.all('p')[2]).to have_content("issue_1") + expect(results_list.all('p')[3]).to have_content("issue_2") + expect(results_list.all('p')[4]).to have_content("issue_4") + end end - it "sorts by priority" do - click_link "Priority" + context 'issues and merge requests' do + let(:issue_1) { create(:issue, id: 10000, title: 'issue_1', project: project) } + let(:issue_2) { create(:issue, id: 10001, title: 'issue_2', project: project) } + let(:merge_request_1) { create(:merge_request, id: 10000, title: 'merge_request_1', source_project: project) } + + before do + issue_1.labels << label_1 + issue_2.labels << label_2 + + create(:todo, user: user, project: project, target: issue_1) + create(:todo, user: user, project: project, target: issue_2) + create(:todo, user: user, project: project, target: merge_request_1) + + login_as(user) + visit dashboard_todos_path + end + + it "doesn't mix issues and merge requests priorities" do + click_link "Priority" - results_list = page.find('.todos-list') - expect(results_list.all('p')[0]).to have_content("issue_3") - expect(results_list.all('p')[1]).to have_content("merge_request_1") - expect(results_list.all('p')[2]).to have_content("issue_1") - expect(results_list.all('p')[3]).to have_content("issue_2") - expect(results_list.all('p')[4]).to have_content("issue_4") + results_list = page.find('.todos-list') + expect(results_list.all('p')[0]).to have_content("issue_1") + expect(results_list.all('p')[1]).to have_content("issue_2") + expect(results_list.all('p')[2]).to have_content("merge_request_1") + end end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index bf93c1d1251..3ae83ac082d 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -13,7 +13,7 @@ describe 'Dashboard Todos', feature: true do visit dashboard_todos_path end it 'shows "All done" message' do - expect(page).to have_content "You're all done!" + expect(page).to have_content "Todos let you see what you should do next." end end @@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message' do - expect(page).to have_content("You're all done!") + expect(page).to have_content("Good job! Looks like you don't have any todos left.") end end @@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message' do - expect(page).to have_content("You're all done!") + expect(page).to have_content("Good job! Looks like you don't have any todos left.") end end end @@ -132,7 +132,6 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message!' do - within('.todos-pending-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') @@ -153,7 +152,7 @@ describe 'Dashboard Todos', feature: true do within('.todos-pending-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content 'Done 0' - expect(page).to have_content "You're all done!" + expect(page).to have_content "Good job! Looks like you don't have any todos left." end end end diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 6fce11de30f..db60c01db0d 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -21,7 +21,7 @@ describe BranchesFinder do result = branches_finder.execute recently_updated_branch = repository.branches.max do |a, b| - repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date end expect(result.first.name).to eq(recently_updated_branch.name) diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb index 2ac810e478a..98b42e264dc 100644 --- a/spec/finders/tags_finder_spec.rb +++ b/spec/finders/tags_finder_spec.rb @@ -20,7 +20,7 @@ describe TagsFinder do result = tags_finder.execute recently_updated_tag = repository.tags.max do |a, b| - repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date end expect(result.first.name).to eq(recently_updated_tag.name) diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index 6bcfdf191c2..a3171353bfb 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require abuse_reports */ /*= require jquery */ diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 6699dee3cf7..9d855ef1060 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 56b98856614..16e908f3a81 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require lib/utils/common_utils */ diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index f5e400a2e39..3d705e1cb2e 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require awards_handler */ /*= require jquery */ diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 78795f7654a..36254a7370e 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require behaviors/autosize */ diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 13babb5bfdb..7370ccb4a08 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require behaviors/quick_submit */ diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 724c3baf989..32469a4fd1f 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require behaviors/requires_input */ diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 8402a996192..6208c2386b0 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require jquery //= require jquery_ujs //= require js.cookie @@ -17,7 +18,10 @@ gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.issueBoards.BoardsStore.create(); - Cookies.set('issue_board_welcome_hidden', 'false'); + Cookies.set('issue_board_welcome_hidden', 'false', { + expires: 365 * 10, + path: '' + }); }); describe('Store', () => { diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index cfece7ea7e8..90cb8926545 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 7b2c0945ce8..1a0427fdd90 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 052455f2ca6..80d05e8a1a3 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ const listObj = { id: 1, position: 0, diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 new file mode 100644 index 00000000000..93f73fa0e9a --- /dev/null +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -0,0 +1,39 @@ +/* eslint-disable */ +/*= require sidebar */ +/*= require jquery */ +/*= require js.cookie */ +/*= require lib/utils/text_utility */ + +((global) => { + describe('Dashboard', () => { + const fixtureTemplate = 'dashboard.html'; + + function todosCountText() { + return $('.js-todos-count').text(); + } + + function triggerToggle(newCount) { + $(document).trigger('todo:toggle', newCount); + } + + fixture.preload(fixtureTemplate); + beforeEach(() => { + fixture.load(fixtureTemplate); + new global.Sidebar(); + }); + + it('should update todos-count after receiving the todo:toggle event', () => { + triggerToggle(5); + expect(todosCountText()).toEqual('5'); + }); + + it('should display todos-count with delimiter', () => { + triggerToggle(1000); + expect(todosCountText()).toEqual('1,000'); + + triggerToggle(1000000); + expect(todosCountText()).toEqual('1,000,000'); + }); + }); + +})(window.gl); diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index a2d1b0a7732..9fdbab3a9e9 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require lib/utils/datetime_utility (() => { describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 22293d4de87..5d817802602 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require vue //= require diff_notes/models/discussion //= require diff_notes/models/note diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js index eced2f6575d..f28983d7764 100644 --- a/spec/javascripts/extensions/array_spec.js +++ b/spec/javascripts/extensions/array_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require extensions/array */ diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index b644344b95a..9c361bb0867 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require extensions/jquery */ diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore new file mode 100644 index 00000000000..009b68d5d1c --- /dev/null +++ b/spec/javascripts/fixtures/.gitignore @@ -0,0 +1 @@ +*.html.raw diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml new file mode 100644 index 00000000000..32446acfd60 --- /dev/null +++ b/spec/javascripts/fixtures/dashboard.html.haml @@ -0,0 +1,45 @@ +%ul.nav.nav-sidebar + %li.home.active + %a.dashboard-shortcuts-projects + %span + Projects + %li + %a + %span + Todos + %span.count.js-todos-count + 1 + %li + %a.dashboard-shortcuts-activity + %span + Activity + %li + %a + %span + Groups + %li + %a + %span + Milestones + %li + %a.dashboard-shortcuts-issues + %span + Issues + %span + 1 + %li + %a.dashboard-shortcuts-merge_requests + %span + Merge Requests + %li + %a + %span + Snippets + %li + %a + %span + Help + %li + %a + %span + Profile Settings diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js index 99e3f7247bd..41cf40c29cf 100644 --- a/spec/javascripts/fixtures/emoji_menu.js +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml new file mode 100644 index 00000000000..4db2ef604de --- /dev/null +++ b/spec/javascripts/fixtures/header.html.haml @@ -0,0 +1,35 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class + .container-fluid + .header-content + %button.side-nav-toggle + %span.sr-only + Toggle navigation + %i.fa.fa-bars + %button.navbar-toggle + %span.sr-only + Toggle navigation + %i.fa.fa-ellipsis-v + .navbar-collapse.collapse + %ui.nav.navbar-nav + %li.hidden-sm.hidden-xs + %li.visible-sm.visible-xs + %li + %a + %i.fa.fa-bell.fa-fw + %span.badge.todos-pending-count + %li + %a + %i.fa.fa-plus.fa-fw + %li.header-user.dropdown + %a + %img + %span.caret + .dropdown-menu-nav + .dropdown-menu-align-right + %ul + %li + %a.profile-link + %li + %a + %li.divider + %li.sign-out-link diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb new file mode 100644 index 00000000000..d95eb851421 --- /dev/null +++ b/spec/javascripts/fixtures/issues.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:project) { create(:project_empty_repo) } + + render_views + + before(:all) do + clean_frontend_fixtures('issues/') + end + + before(:each) do + sign_in(admin) + end + + it 'issues/open-issue.html.raw' do |example| + render_issue(example.description, create(:issue, project: project)) + end + + it 'issues/closed-issue.html.raw' do |example| + render_issue(example.description, create(:closed_issue, project: project)) + end + + it 'issues/issue-with-task-list.html.raw' do |example| + issue = create(:issue, project: project) + issue.update(description: '- [ ] Task List Item') + render_issue(example.description, issue) + end + + private + + def render_issue(fixture_file_name, issue) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: issue.to_param + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml deleted file mode 100644 index 06c2ab1e823..00000000000 --- a/spec/javascripts/fixtures/issues_show.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -:css - .hidden { display: none !important; } - -.flash-container.flash-container-page - .flash-alert - .flash-notice - -.status-box.status-box-open Open -.status-box.status-box-closed.hidden Closed -%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close -%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen - -.detail-page-description - .description.js-task-list-container - .wiki - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - %textarea.js-task-list-field - \- [ ] Task List Item - -%form.js-issuable-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml index 95efaff4b69..d48b77cf0ce 100644 --- a/spec/javascripts/fixtures/right_sidebar.html.haml +++ b/spec/javascripts/fixtures/right_sidebar.html.haml @@ -5,6 +5,10 @@ %div.block.issuable-sidebar-header %a.gutter-toggle.pull-right.js-sidebar-toggle %i.fa.fa-angle-double-left + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: "1", issuable_type: "issue", url: "/todos" }} + %span.js-issuable-todo-text + Add Todo + %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden %form.issuable-context-form %div.block.labels diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json new file mode 100644 index 00000000000..62c2387d515 --- /dev/null +++ b/spec/javascripts/fixtures/todos.json @@ -0,0 +1,4 @@ +{ + "count": 1, + "delete_path": "/dashboard/todos/1" +}
\ No newline at end of file diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index b529ea6458d..8ba238018cd 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require jquery */ /*= require gl_dropdown */ /*= require turbolinks */ @@ -6,6 +7,7 @@ (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + const SEARCH_INPUT_SELECTOR = '.dropdown-input-field'; const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`; @@ -16,6 +18,8 @@ ESC: 27 }; + let remoteCallback; + let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) { i = i || 0; if (!i) direction = direction.toUpperCase(); @@ -32,18 +36,19 @@ } }; + let remoteMock = function remoteMock(data, term, callback) { + remoteCallback = callback.bind({}, data); + } + describe('Dropdown', function describeDropdown() { fixture.preload('gl_dropdown.html'); fixture.preload('projects.json'); - beforeEach(() => { - fixture.load('gl_dropdown.html'); - this.dropdownContainerElement = $('.dropdown.inline'); - this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); - this.projectsData = fixture.load('projects.json')[0]; + function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ selectable: true, - data: this.projectsData, + filterable: isFilterable, + data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, text: (project) => { (project.name_with_namespace || project.name); }, @@ -51,6 +56,13 @@ project.id; } }); + } + + beforeEach(() => { + fixture.load('gl_dropdown.html'); + this.dropdownContainerElement = $('.dropdown.inline'); + this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); + this.projectsData = fixture.load('projects.json')[0]; }); afterEach(() => { @@ -59,6 +71,7 @@ }); it('should open on click', () => { + initDropDown.call(this, false); expect(this.dropdownContainerElement).not.toHaveClass('open'); this.dropdownButtonElement.click(); expect(this.dropdownContainerElement).toHaveClass('open'); @@ -66,26 +79,27 @@ describe('that is open', () => { beforeEach(() => { + initDropDown.call(this, false, false); this.dropdownButtonElement.click(); }); it('should select a following item on DOWN keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0); + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); navigateWithKeys('down', randomIndex, () => { - expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused'); + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); }); it('should select a previous item on UP keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0); + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); navigateWithKeys('down', (this.projectsData.length - 1), () => { - expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); navigateWithKeys('up', randomIndex, () => { - expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused'); + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); }); }); @@ -97,7 +111,7 @@ spyOn(Turbolinks, 'visit').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); - let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement); + let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); let linkedLocation = link.attr('href'); if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); @@ -115,5 +129,42 @@ expect(this.dropdownContainerElement).not.toHaveClass('open'); }); }); + + describe('opened and waiting for a remote callback', () => { + beforeEach(() => { + initDropDown.call(this, true, true); + this.dropdownButtonElement.click(); + }); + + it('should not focus search input while remote task is not complete', ()=> { + expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus search input after remote task is complete', ()=> { + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus on input when opening for the second time', ()=> { + remoteCallback(); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC + }); + this.dropdownButtonElement.click(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + }); + + describe('input focus with array data', () => { + it('should focus input when passing array data to drop down', ()=> { + initDropDown.call(this, false, true); + this.dropdownButtonElement.click(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + }); }); })(); diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index da9259edd78..4bdd72800ea 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require jquery //= require gl_field_errors diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index d5401fbb0d1..8c66c45ba79 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 56970e22e34..920e4ee0892 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 4b05d401a42..ae2821ecad9 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js new file mode 100644 index 00000000000..9a859655d8b --- /dev/null +++ b/spec/javascripts/header_spec.js @@ -0,0 +1,55 @@ +/* eslint-disable */ +/*= require header */ +/*= require lib/utils/text_utility */ +/*= require jquery */ + +(function() { + + describe('Header', function() { + var todosPendingCount = '.todos-pending-count'; + var fixtureTemplate = 'header.html'; + + function isTodosCountHidden() { + return $(todosPendingCount).hasClass('hidden'); + } + + function triggerToggle(newCount) { + $(document).trigger('todo:toggle', newCount); + } + + fixture.preload(fixtureTemplate); + beforeEach(function() { + fixture.load(fixtureTemplate); + }); + + it('should update todos-pending-count after receiving the todo:toggle event', function() { + triggerToggle(5); + expect($(todosPendingCount).text()).toEqual('5'); + }); + + it('should hide todos-pending-count when it is 0', function() { + triggerToggle(0); + expect(isTodosCountHidden()).toEqual(true); + }); + + it('should show todos-pending-count when it is more than 0', function() { + triggerToggle(10); + expect(isTodosCountHidden()).toEqual(false); + }); + + describe('when todos-pending-count is 1000', function() { + beforeEach(function() { + triggerToggle(1000); + }); + + it('should show todos-pending-count', function() { + expect(isTodosCountHidden()).toEqual(false); + }); + + it('should add delimiter to todos-pending-count', function() { + expect($(todosPendingCount).text()).toEqual('1,000'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 33690c7a5f3..949114185cf 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,118 +1,163 @@ +/* eslint-disable */ /*= require lib/utils/text_utility */ /*= require issue */ (function() { + var INVALID_URL = 'http://goesnowhere.nothing/whereami'; + var $boxClosed, $boxOpen, $btnClose, $btnReopen; + + fixture.preload('issues/closed-issue.html'); + fixture.preload('issues/issue-with-task-list.html'); + fixture.preload('issues/open-issue.html'); + + function expectErrorMessage() { + var $flashMessage = $('div.flash-alert'); + expect($flashMessage).toExist(); + expect($flashMessage).toBeVisible(); + expect($flashMessage).toHaveText('Unable to update this issue at this time.'); + } + + function expectIssueState(isIssueOpen) { + expectVisibility($boxClosed, !isIssueOpen); + expectVisibility($boxOpen, isIssueOpen); + + expectVisibility($btnClose, isIssueOpen); + expectVisibility($btnReopen, !isIssueOpen); + } + + function expectPendingRequest(req, $triggeredButton) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe($triggeredButton.attr('href')); + expect($triggeredButton).toHaveProp('disabled', true); + } + + function expectVisibility($element, shouldBeVisible) { + if (shouldBeVisible) { + expect($element).not.toHaveClass('hidden'); + } else { + expect($element).toHaveClass('hidden'); + } + } + + function findElements() { + $boxClosed = $('div.status-box-closed'); + expect($boxClosed).toExist(); + expect($boxClosed).toHaveText('Closed'); + + $boxOpen = $('div.status-box-open'); + expect($boxOpen).toExist(); + expect($boxOpen).toHaveText('Open'); + + $btnClose = $('.btn-close.btn-grouped'); + expect($btnClose).toExist(); + expect($btnClose).toHaveText('Close issue'); + + $btnReopen = $('.btn-reopen.btn-grouped'); + expect($btnReopen).toExist(); + expect($btnReopen).toHaveText('Reopen issue'); + } + describe('Issue', function() { - return describe('task lists', function() { - fixture.preload('issues_show.html'); + describe('task lists', function() { + fixture.load('issues/issue-with-task-list.html'); beforeEach(function() { - fixture.load('issues_show.html'); - return this.issue = new Issue(); + this.issue = new Issue(); }); + it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); $('input[type=checkbox]').attr('checked', true).trigger('change'); - return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { + + it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('/foo'); - return expect(req.data.issue.description).not.toBe(null); + expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json'); + expect(req.data.issue.description).not.toBe(null); }); - return $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); }); - describe('reopen/close issue', function() { - fixture.preload('issues_show.html'); + describe('close issue', function() { beforeEach(function() { - fixture.load('issues_show.html'); - return this.issue = new Issue(); + fixture.load('issues/open-issue.html'); + findElements(); + this.issue = new Issue(); + + expectIssueState(true); }); + it('closes an issue', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://gitlab.com/issues/6/close'); - return req.success({ + expectPendingRequest(req, $btnClose); + req.success({ id: 34 }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); - expect($btnReopen).toBeVisible(); - expect($btnClose).toBeHidden(); - expect($('div.status-box-closed')).toBeVisible(); - return expect($('div.status-box-open')).toBeHidden(); + + expectIssueState(false); + expect($btnClose).toHaveProp('disabled', false); }); + it('fails to close an issue with success:false', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://goesnowhere.nothing/whereami'); - return req.success({ + expectPendingRequest(req, $btnClose); + req.success({ saved: false }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + + $btnClose.attr('href', INVALID_URL); $btnClose.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-closed')).toBeHidden(); - expect($('div.status-box-open')).toBeVisible(); - expect($('div.flash-alert')).toBeVisible(); - return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + + expectIssueState(true); + expect($btnClose).toHaveProp('disabled', false); + expectErrorMessage(); }); + it('fails to closes an issue with HTTP error', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://goesnowhere.nothing/whereami'); - return req.error(); + expectPendingRequest(req, $btnClose); + req.error(); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + + $btnClose.attr('href', INVALID_URL); $btnClose.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-closed')).toBeHidden(); - expect($('div.status-box-open')).toBeVisible(); - expect($('div.flash-alert')).toBeVisible(); - return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + + expectIssueState(true); + expect($btnClose).toHaveProp('disabled', true); + expectErrorMessage(); }); - return it('reopens an issue', function() { - var $btnClose, $btnReopen; + }); + + describe('reopen issue', function() { + beforeEach(function() { + fixture.load('issues/closed-issue.html'); + findElements(); + this.issue = new Issue(); + + expectIssueState(false); + }); + + it('reopens an issue', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://gitlab.com/issues/6/reopen'); - return req.success({ + expectPendingRequest(req, $btnReopen); + req.success({ id: 34 }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - expect($btnReopen.text()).toBe('Reopen'); + $btnReopen.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-open')).toBeVisible(); - return expect($('div.status-box-closed')).toBeHidden(); + + expectIssueState(true); + expect($btnReopen).toHaveProp('disabled', false); }); }); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 1ad6f612210..49687048eb5 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable */ //= require lib/utils/type_utility //= require jquery //= require bootstrap diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index e2789571607..e0192a2d624 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require line_highlighter */ diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 61830d267a9..83d279ab414 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require merge_request */ diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 96ee5235acf..6a53c6aa6ac 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require merge_request_tabs */ //= require breakpoints diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index c9175e2b704..1e2072f370a 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require merge_request_widget */ /*= require lib/utils/jquery.timeago.js */ diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index f09596bd36d..c092424ec32 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require jquery-ui/autocomplete */ /*= require new_branch_form */ diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index a588f403dd5..2e3a4b66e2d 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require notes */ /*= require autosize */ /*= require gl_form */ diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 51eb12b41d4..1963857bba3 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require bootstrap */ /*= require select2 */ diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 5aa8d5b5826..ef03d1147de 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,11 @@ +/* eslint-disable */ /*= require right_sidebar */ /*= require jquery */ /*= require js.cookie */ +/*= require extensions/jquery.js */ + (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; @@ -55,12 +58,27 @@ $labelsIcon.click(); return assertSidebarState('expanded'); }); - return it('should collapse when the icon arrow clicked while it is floating on page', function() { + it('should collapse when the icon arrow clicked while it is floating on page', function() { $labelsIcon.click(); assertSidebarState('expanded'); $toggle.click(); return assertSidebarState('collapsed'); }); + + it('should broadcast todo:toggle event when add todo clicked', function() { + spyOn(jQuery, 'ajax').and.callFake(function() { + var d = $.Deferred(); + var response = fixture.load('todos.json'); + d.resolve(response); + return d.promise(); + }); + + var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); + + $('.js-issuable-todo').click(); + + expect(todoToggleSpy.calls.count()).toEqual(1); + }) }); }).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 333128782a2..29080804960 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require gl_dropdown */ /*= require search_autocomplete */ diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 04ccf246052..1f36a048153 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require shortcuts_issuable */ diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 8801c297887..9cb8243ee2c 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // PhantomJS (Teaspoons default driver) doesn't have support for // Function.prototype.bind, which has caused confusion. Use this polyfill to // avoid the confusion. @@ -27,7 +28,7 @@ // setTimeout(Teaspoon.execute, 1000) // Matching files // By default Teaspoon will look for files that match -// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path // and it'll be included in the default suite automatically. If you want to // customize suites, check out the configuration in teaspoon_env.rb // Manifest diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 4e5dd1e59bf..498f0f06797 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require syntax_highlight */ diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 784b43d4846..024a91f0a80 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require u2f/authenticate */ /*= require u2f/util */ diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index ca91a716ba3..ad133682fb1 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,3 +1,4 @@ +/* eslint-disable */ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 01d6b7a8961..abea76f622f 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require u2f/register */ /*= require u2f/util */ diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 0c1266800d7..65b6e3dce33 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ /*= require zen_mode */ diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 2f9343fadaf..fbf7a461fa5 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -8,6 +8,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do end shared_examples_for "external issue tracker" do + it_behaves_like 'a reference containing an element node' + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index a2025672ad9..8f0b2db3e8e 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -22,6 +22,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'internal reference' do + it_behaves_like 'a reference containing an element node' + let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do @@ -83,6 +85,20 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(link.attr('data-issue')).to eq issue.id.to_s end + it 'includes a data-original attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-original') + expect(link.attr('data-original')).to eq reference + end + + it 'does not escape the data-original attribute' do + inner_html = 'element <code>node</code> inside' + doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>}) + expect(doc.children.first.attr('data-original')).to eq inner_html + end + it 'supports an :only_path context' do doc = reference_filter("Issue #{reference}", only_path: true) link = doc.css('a').first.attr('href') @@ -101,6 +117,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project reference' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } @@ -141,6 +159,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project URL reference' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } @@ -160,39 +180,45 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project reference in link href' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} } + let(:reference) { issue.to_reference(project) } + let(:reference_link) { %{<a href="#{reference}">Reference</a>} } it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + doc = reference_filter("See #{reference_link}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project2) end it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference_link}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end end context 'cross-project URL in link href' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} } + let(:reference) { "#{helper.url_for_issue(issue.iid, project2) + "#note_123"}" } + let(:reference_link) { %{<a href="#{reference}">Reference</a>} } it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + doc = reference_filter("See #{reference_link}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project2) + "#note_123" end it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference_link}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 729e77fd43f..5bfeb82e738 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -24,6 +24,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning @all' do + it_behaves_like 'a reference containing an element node' + let(:reference) { User.reference_prefix + 'all' } before do @@ -60,6 +62,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning a user' do + it_behaves_like 'a reference containing an element node' + it 'links to a User' do doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) @@ -89,6 +93,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning a group' do + it_behaves_like 'a reference containing an element node' + let(:group) { create(:group) } let(:reference) { group.to_reference } diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb new file mode 100644 index 00000000000..2501b638774 --- /dev/null +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +describe Banzai::Pipeline::FullPipeline do + describe 'References' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } + + it 'handles markdown inside a reference' do + markdown = "[some `code` inside](#{issue.to_reference})" + result = described_class.call(markdown, project: project) + link_content = result[:output].css('a').inner_html + expect(link_content).to eq('some <code>code</code> inside') + end + + it 'sanitizes reference HTML' do + link_label = '<script>bad things</script>' + markdown = "[#{link_label}](#{issue.to_reference})" + result = described_class.to_html(markdown, project: project) + expect(result).not_to include(link_label) + end + + it 'escapes the data-original attribute on a reference' do + markdown = %Q{[">bad things](#{issue.to_reference})} + result = described_class.to_html(markdown, project: project) + expect(result).to include(%{data-original='\">bad things'}) + end + end +end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 254657a881d..6d2c141e18b 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -6,39 +6,60 @@ describe Banzai::Redactor do let(:redactor) { described_class.new(project, user) } describe '#redact' do - it 'redacts an Array of documents' do - doc1 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">foo</a>') - - doc2 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">bar</a>') - - expect(redactor).to receive(:nodes_visible_to_user).and_return([]) - - redacted_data = redactor.redact([doc1, doc2]) - - expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) - expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0]) - expect(doc1.to_html).to eq('foo') - expect(doc2.to_html).to eq('bar') + context 'when reference not visible to user' do + before do + expect(redactor).to receive(:nodes_visible_to_user).and_return([]) + end + + it 'redacts an array of documents' do + doc1 = Nokogiri::HTML. + fragment('<a class="gfm" data-reference-type="issue">foo</a>') + + doc2 = Nokogiri::HTML. + fragment('<a class="gfm" data-reference-type="issue">bar</a>') + + redacted_data = redactor.redact([doc1, doc2]) + + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0]) + expect(doc1.to_html).to eq('foo') + expect(doc2.to_html).to eq('bar') + end + + it 'replaces redacted reference with inner HTML' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>") + redactor.redact([doc]) + expect(doc.to_html).to eq('foo') + end + + context 'when data-original attribute provided' do + let(:original_content) { '<code>foo</code>' } + it 'replaces redacted reference with original content' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>") + redactor.redact([doc]) + expect(doc.to_html).to eq(original_content) + end + end end - it 'does not redact an Array of documents' do - doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' - doc1 = Nokogiri::HTML.fragment(doc1_html) + context 'when reference visible to user' do + it 'does not redact an array of documents' do + doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' + doc1 = Nokogiri::HTML.fragment(doc1_html) - doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>' - doc2 = Nokogiri::HTML.fragment(doc2_html) + doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>' + doc2 = Nokogiri::HTML.fragment(doc2_html) - nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] } - expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten) + nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] } + expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten) - redacted_data = redactor.redact([doc1, doc2]) + redacted_data = redactor.redact([doc1, doc2]) - expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) - expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1]) - expect(doc1.to_html).to eq(doc1_html) - expect(doc2.to_html).to eq(doc2_html) + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1]) + expect(doc1.to_html).to eq(doc1_html) + expect(doc2.to_html).to eq(doc2_html) + end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 1af553f8f03..7478f86bd28 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -2,6 +2,10 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do describe '#execute' do + before do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + end + context 'when an error occurs' do let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } let(:octocat) { double(id: 123456, login: 'octocat') } @@ -152,9 +156,9 @@ describe Gitlab::GithubImport::Importer, lib: true do message: 'The remote data could not be fully imported.', errors: [ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, - { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" }, + { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, + { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } ] diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index feee0f025d8..07a2c316899 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -178,6 +178,7 @@ Ci::Pipeline: - finished_at - duration - user_id +- lock_version CommitStatus: - id - project_id @@ -217,6 +218,7 @@ CommitStatus: - yaml_variables - queued_at - token +- lock_version Ci::Variable: - id - project_id diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb new file mode 100644 index 00000000000..498dc514c8c --- /dev/null +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::OptimisticLocking, lib: true do + describe '#retry_lock' do + let!(:pipeline) { create(:ci_pipeline) } + let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + + it 'does not reload object if state changes' do + expect(pipeline).not_to receive(:reload) + expect(pipeline).to receive(:succeed).and_call_original + + described_class.retry_lock(pipeline) do |subject| + subject.succeed + end + end + + it 'retries action if exception is raised' do + pipeline.succeed + + expect(pipeline2).to receive(:reload).and_call_original + expect(pipeline2).to receive(:drop).twice.and_call_original + + described_class.retry_lock(pipeline2) do |subject| + subject.drop + end + end + + it 'raises exception when too many retries' do + expect(pipeline).to receive(:drop).twice.and_call_original + + expect do + described_class.retry_lock(pipeline, 1) do |subject| + subject.lock_version = 100 + subject.drop + end + end.to raise_error(ActiveRecord::StaleObjectError) + end + end +end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 74ff85e132a..e5406fb2d33 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -116,6 +116,55 @@ describe Gitlab::Redis do end end + describe '#sentinels' do + subject { described_class.new(Rails.env).sentinels } + + context 'when sentinels are defined' do + let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + + it 'returns an array of hashes with host and port keys' do + stub_const("#{described_class}::CONFIG_FILE", config) + + is_expected.to include(host: 'localhost', port: 26380) + is_expected.to include(host: 'slave2', port: 26381) + end + end + + context 'when sentinels are not defined' do + let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + + it 'returns nil' do + stub_const("#{described_class}::CONFIG_FILE", config) + + is_expected.to be_nil + end + end + end + + describe '#sentinels?' do + subject { described_class.new(Rails.env).sentinels? } + + context 'when sentinels are defined' do + let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') } + + it 'returns true' do + stub_const("#{described_class}::CONFIG_FILE", config) + + is_expected.to be_truthy + end + end + + context 'when sentinels are not defined' do + let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') } + + it 'returns false' do + stub_const("#{described_class}::CONFIG_FILE", config) + + is_expected.to be_falsey + end + end + end + describe '#raw_config_hash' do it 'returns default redis url when no config file is present' do expect(subject).to receive(:fetch_config) { false } diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb new file mode 100644 index 00000000000..d5d87310874 --- /dev/null +++ b/spec/lib/gitlab/utils_spec.rb @@ -0,0 +1,35 @@ +describe Gitlab::Utils, lib: true do + def to_boolean(value) + described_class.to_boolean(value) + end + + describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + + it 'converts a valid string to a boolean' do + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean('oFF')).to be(false) + end + + it 'converts an invalid string to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 43397c5ae39..5eb14dc6bd2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -138,32 +138,26 @@ describe Ci::Pipeline, models: true do describe 'state machine' do let(:current) { Time.now.change(usec: 0) } - let(:build) { create_build('build1', current, 10) } - let(:build_b) { create_build('build2', current, 20) } - let(:build_c) { create_build('build3', current + 50, 10) } + let(:build) { create_build('build1', 0) } + let(:build_b) { create_build('build2', 0) } + let(:build_c) { create_build('build3', 0) } describe '#duration' do before do - pipeline.update(created_at: current) - - travel_to(current + 5) do - pipeline.run - pipeline.save - end - travel_to(current + 30) do - build.success + build.run! + build.success! + build_b.run! + build_c.run! end travel_to(current + 40) do - build_b.drop + build_b.drop! end travel_to(current + 70) do - build_c.success + build_c.success! end - - pipeline.drop end it 'matches sum of builds duration' do @@ -455,7 +449,9 @@ describe Ci::Pipeline, models: true do context 'when all builds succeed' do before do build_a.success - build_b.success + + # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker + build_b.reload.success end it 'receives a success event once' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 60e4bbc8564..a59d30687f6 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -298,6 +298,20 @@ describe Issue, "Issuable" do end end + describe '.order_labels_priority' do + let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) } + + subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority } + + before do + issue.labels << label_1 + issue.labels << label_2 + end + + it { is_expected.to eq(2) } + end + describe ".with_label" do let(:project) { create(:project, :public) } let(:bug) { create(:label, project: project, title: 'bug') } diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 5363aea4d22..9041690023f 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -22,4 +22,18 @@ describe ProjectFeaturesCompatibility do expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end + + it "converts fields from true to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, true) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from false to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, false) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ac862055ebc..47f89f744cb 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -265,4 +265,10 @@ describe Group, models: true do members end + + describe '#web_url' do + it 'returns the canonical URL' do + expect(group.web_url).to include("groups/#{group.name}") + end + end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index f6b2ec5ae31..68f72f5c86e 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -57,12 +57,12 @@ describe ProjectMember, models: true do it "creates an expired event when left due to expiry" do expired = create(:project_member, project: project, expires_at: Time.now - 6.days) expired.destroy - expect(Event.first.action).to eq(Event::EXPIRED) + expect(Event.recent.first.action).to eq(Event::EXPIRED) end it "creates a left event when left due to leave" do master.destroy - expect(Event.first.action).to eq(Event::LEFT) + expect(Event.recent.first.action).to eq(Event::LEFT) end it "destroys itself and delete associated todos" do diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 6ff32aea018..a9f637147d1 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -10,23 +10,15 @@ describe JiraService, models: true do context 'when service is active' do before { subject.active = true } - it { is_expected.to validate_presence_of(:api_url) } - it { is_expected.to validate_presence_of(:project_url) } - it { is_expected.to validate_presence_of(:issues_url) } - it { is_expected.to validate_presence_of(:new_issue_url) } - it_behaves_like 'issue tracker service URL attribute', :api_url - it_behaves_like 'issue tracker service URL attribute', :project_url - it_behaves_like 'issue tracker service URL attribute', :issues_url - it_behaves_like 'issue tracker service URL attribute', :new_issue_url + it { is_expected.to validate_presence_of(:url) } + it { is_expected.to validate_presence_of(:project_key) } + it_behaves_like 'issue tracker service URL attribute', :url end context 'when service is inactive' do before { subject.active = false } - it { is_expected.not_to validate_presence_of(:api_url) } - it { is_expected.not_to validate_presence_of(:project_url) } - it { is_expected.not_to validate_presence_of(:issues_url) } - it { is_expected.not_to validate_presence_of(:new_issue_url) } + it { is_expected.not_to validate_presence_of(:url) } end end @@ -50,23 +42,25 @@ describe JiraService, models: true do project_id: project.id, project: project, service_hook: true, - project_url: 'http://jira.example.com', + url: 'http://jira.example.com', username: 'gitlab_jira_username', password: 'gitlab_jira_password' ) - @jira_service.save # will build API URL, as api_url was not specified above - @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) - # https://github.com/bblimke/webmock#request-with-basic-authentication - @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' + + @jira_service.save + + project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' + @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' - WebMock.stub_request(:post, @api_url) + WebMock.stub_request(:get, project_url) + WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) end it "calls JIRA API" do - @jira_service.execute(merge_request, - ExternalIssue.new("JIRA-123", project)) + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + expect(WebMock).to have_requested(:post, @comment_url).with( body: /Issue solved with/ ).once @@ -74,9 +68,9 @@ describe JiraService, models: true do it "calls the api with jira_issue_transition_id" do @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' - @jira_service.execute(merge_request, - ExternalIssue.new("JIRA-123", project)) - expect(WebMock).to have_requested(:post, @api_url).with( + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @transitions_url).with( body: /this-is-a-custom-id/ ).once end @@ -90,7 +84,7 @@ describe JiraService, models: true do @jira_service = JiraService.create!( project: create(:project), properties: { - api_url: 'http://jira.example.com/rest/api/2', + url: 'http://jira.example.com/rest/api/2', username: 'mic', password: "password" } @@ -98,7 +92,7 @@ describe JiraService, models: true do end it "reset password if url changed" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.save expect(@jira_service.password).to be_nil end @@ -110,16 +104,16 @@ describe JiraService, models: true do end it "does not reset password if new url is set together with password, even if it's the same password" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save expect(@jira_service.password).to eq("password") - expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") end it "resets password if url changed, even if setter called multiple times" do - @jira_service.api_url = 'http://jira1.example.com/rest/api/2' - @jira_service.api_url = 'http://jira1.example.com/rest/api/2' + @jira_service.url = 'http://jira1.example.com/rest/api/2' + @jira_service.url = 'http://jira1.example.com/rest/api/2' @jira_service.save expect(@jira_service.password).to be_nil end @@ -130,18 +124,18 @@ describe JiraService, models: true do @jira_service = JiraService.create( project: create(:project), properties: { - api_url: 'http://jira.example.com/rest/api/2', + url: 'http://jira.example.com/rest/api/2', username: 'mic' } ) end it "saves password if new url is set together with password" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save expect(@jira_service.password).to eq("password") - expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") end end end @@ -152,9 +146,7 @@ describe JiraService, models: true do subject.active = true end - it { is_expected.to validate_presence_of :project_url } - it { is_expected.to validate_presence_of :issues_url } - it { is_expected.to validate_presence_of :new_issue_url } + it { is_expected.to validate_presence_of :url } end end @@ -201,9 +193,7 @@ describe JiraService, models: true do settings = { "jira" => { "title" => "Jira", - "project_url" => "http://jira.sample/projects/project_a", - "issues_url" => "http://jira.sample/issues/:id", - "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" + "url" => "http://jira.sample/projects/project_a" } } allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) @@ -215,9 +205,8 @@ describe JiraService, models: true do end it 'is prepopulated with the settings' do - expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a') - expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id") - expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new") + expect(@service.properties["title"]).to eq('Jira') + expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a') end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4dda1ee558..aef277357cf 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,11 +67,11 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } - context 'after create' do - it "creates project feature" do + context 'after initialized' do + it "has a project_feature" do project = FactoryGirl.build(:project) - expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + expect(project.project_feature.present?).to be_present end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 187a1bf2d79..04b7d19d414 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -68,8 +68,8 @@ describe Repository, models: true do double_first = double(committed_date: Time.now) double_last = double(committed_date: Time.now - 1.second) - allow(tag_a).to receive(:target).and_return(double_first) - allow(tag_b).to receive(:target).and_return(double_last) + allow(tag_a).to receive(:dereferenced_target).and_return(double_first) + allow(tag_b).to receive(:dereferenced_target).and_return(double_last) allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end @@ -83,8 +83,8 @@ describe Repository, models: true do double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) - allow(tag_a).to receive(:target).and_return(double_last) - allow(tag_b).to receive(:target).and_return(double_first) + allow(tag_a).to receive(:dereferenced_target).and_return(double_last) + allow(tag_b).to receive(:dereferenced_target).and_return(double_first) allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end @@ -632,9 +632,9 @@ describe Repository, models: true do context "when the branch wasn't empty" do it 'updates the head' do - expect(repository.find_branch('feature').target.id).to eq(old_rev) + expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) repository.update_branch_with_hooks(user, 'feature') { new_rev } - expect(repository.find_branch('feature').target.id).to eq(new_rev) + expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end end @@ -659,7 +659,7 @@ describe Repository, models: true do context 'when the update would remove commits from the target branch' do it 'raises an exception' do branch = 'master' - old_rev = repository.find_branch(branch).target.sha + old_rev = repository.find_branch(branch).dereferenced_target.sha # The 'master' branch is NOT an ancestor of new_rev. expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) @@ -1472,4 +1472,14 @@ describe Repository, models: true do end.to raise_error(Repository::CommitError) end end + + describe '#remove_storage_from_path' do + let(:storage_path) { project.repository_storage_path } + let(:project_path) { project.path_with_namespace } + let(:full_path) { File.join(storage_path, project_path) } + + it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) } + it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) } + it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') } + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 10c39b90212..d1ed774a914 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -599,6 +599,80 @@ describe User, models: true do end end + describe '.search_with_secondary_emails' do + def search_with_secondary_emails(query) + described_class.search_with_secondary_emails(query) + end + + let!(:user) { create(:user) } + let!(:email) { create(:email) } + + it 'returns users with a matching name' do + expect(search_with_secondary_emails(user.name)).to eq([user]) + end + + it 'returns users with a partially matching name' do + expect(search_with_secondary_emails(user.name[0..2])).to eq([user]) + end + + it 'returns users with a matching name regardless of the casing' do + expect(search_with_secondary_emails(user.name.upcase)).to eq([user]) + end + + it 'returns users with a matching email' do + expect(search_with_secondary_emails(user.email)).to eq([user]) + end + + it 'returns users with a partially matching email' do + expect(search_with_secondary_emails(user.email[0..2])).to eq([user]) + end + + it 'returns users with a matching email regardless of the casing' do + expect(search_with_secondary_emails(user.email.upcase)).to eq([user]) + end + + it 'returns users with a matching username' do + expect(search_with_secondary_emails(user.username)).to eq([user]) + end + + it 'returns users with a partially matching username' do + expect(search_with_secondary_emails(user.username[0..2])).to eq([user]) + end + + it 'returns users with a matching username regardless of the casing' do + expect(search_with_secondary_emails(user.username.upcase)).to eq([user]) + end + + it 'returns users with a matching whole secondary email' do + expect(search_with_secondary_emails(email.email)).to eq([email.user]) + end + + it 'returns users with a matching part of secondary email' do + expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user]) + end + + it 'return users with a matching part of secondary email regardless of case' do + expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user]) + expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user]) + expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user]) + end + + it 'returns multiple users with matching secondary emails' do + email1 = create(:email, email: '1_testemail@example.com') + email2 = create(:email, email: '2_testemail@example.com') + email3 = create(:email, email: 'other@email.com') + email3.user.update_attributes!(email: 'another@mail.com') + + expect( + search_with_secondary_emails('testemail@example.com').map(&:id) + ).to include(email1.user.id, email2.user.id) + + expect( + search_with_secondary_emails('testemail@example.com').map(&:id) + ).not_to include(email3.user.id) + end + end + describe 'by_username_or_id' do let(:user1) { create(:user, username: 'foo') } diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb new file mode 100644 index 00000000000..2b7b6cad654 --- /dev/null +++ b/spec/policies/issues_policy_spec.rb @@ -0,0 +1,193 @@ +require 'spec_helper' + +describe IssuePolicy, models: true do + let(:guest) { create(:user) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:reporter) { create(:user) } + let(:group) { create(:group, :public) } + let(:reporter_from_group_link) { create(:user) } + + def permissions(user, issue) + IssuePolicy.abilities(user, issue).to_set + end + + context 'a private project' do + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [author, :guest] + project.team << [assignee, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'does not allow non-members to read issues' do + expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow non-members to read confidential issues' do + expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end + + context 'a public project' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporter from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end +end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 0f41f8dc7f1..01bb9e955e0 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -265,29 +265,6 @@ describe API::Helpers, api: true do end end - describe '.to_boolean' do - it 'converts a valid string to a boolean' do - expect(to_boolean('true')).to be_truthy - expect(to_boolean('YeS')).to be_truthy - expect(to_boolean('t')).to be_truthy - expect(to_boolean('1')).to be_truthy - expect(to_boolean('ON')).to be_truthy - expect(to_boolean('FaLse')).to be_falsy - expect(to_boolean('F')).to be_falsy - expect(to_boolean('NO')).to be_falsy - expect(to_boolean('n')).to be_falsy - expect(to_boolean('0')).to be_falsy - expect(to_boolean('oFF')).to be_falsy - end - - it 'converts an invalid string to nil' do - expect(to_boolean('fals')).to be_nil - expect(to_boolean('yeah')).to be_nil - expect(to_boolean('')).to be_nil - expect(to_boolean(nil)).to be_nil - end - end - describe '.handle_api_exception' do before do allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 905f762d578..1711096f4bd 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -95,18 +95,6 @@ describe API::API, api: true do expect(json_response['developers_can_push']).to eq(true) expect(json_response['developers_can_merge']).to eq(true) end - - it 'protects a single branch and developers cannot push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: 'tru', developers_can_merge: 'tr' - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end end context 'for an existing protected branch' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 95c7bbf99c9..fc72a44d663 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -277,6 +277,7 @@ describe API::API, api: true do context 'with regular branch' do before do + pipeline.reload pipeline.update(ref: 'master', sha: project.commit('master').sha) @@ -288,6 +289,7 @@ describe API::API, api: true do context 'with branch name containing slash' do before do + pipeline.reload pipeline.update(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 1e21a32a062..a3fc23ba177 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -101,11 +101,11 @@ module Ci it 'equalises number of running builds' do # after finishing the first build for project 1, get a second build from the same project expect(service.execute(shared_runner)).to eq(build1_project1) - build1_project1.success + build1_project1.reload.success expect(service.execute(shared_runner)).to eq(build2_project1) expect(service.execute(shared_runner)).to eq(build1_project2) - build1_project2.success + build1_project2.reload.success expect(service.execute(shared_runner)).to eq(build2_project2) expect(service.execute(shared_runner)).to eq(build1_project3) expect(service.execute(shared_runner)).to eq(build3_project1) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index ad5170afc21..45bc44ba172 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -451,11 +451,7 @@ describe GitPushService, services: true do # project.create_jira_service doesn't seem to invalidate the cache here project.has_external_issue_tracker = true jira_service_settings - - WebMock.stub_request(:post, jira_api_transition_url) - WebMock.stub_request(:post, jira_api_comment_url) - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - WebMock.stub_request(:get, jira_api_test_url) + stub_jira_urls("JIRA-1") allow(closing_commit).to receive_messages({ issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), @@ -475,9 +471,9 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\nrelated to JIRA-1" } it "initiates one api call to jira server to mention the issue" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, @oldrev, @newrev, @ref) - expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: /mentioned this issue in/ ).once end @@ -485,22 +481,19 @@ describe GitPushService, services: true do context "closing an issue" do let(:message) { "this is some work.\n\ncloses JIRA-1" } - let(:transition_body) { { transition: { id: '2' } }.to_json } let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } context "using right markdown" do it "initiates one api call to jira server to close the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_transition_url).with( - body: transition_body - ).once + expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once end it "initiates one api call to jira server to comment on the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: comment_body ).once end @@ -512,15 +505,13 @@ describe GitPushService, services: true do it "does not initiates one api call to jira server to close the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with( - body: transition_body - ) + expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1')) end it "does not initiates one api call to jira server to comment on the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: comment_body ).once end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index a4fcd44882d..0879e3ab4c8 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -37,65 +37,138 @@ describe GitTagPushService, services: true do end describe "Git Tag Push Data" do - before do - service.execute - @push_data = service.push_data - @tag_name = Gitlab::Git.ref_name(ref) - @tag = project.repository.find_tag(@tag_name) - @commit = project.commit(@tag.target) - end - subject { @push_data } + let(:tag) { project.repository.find_tag(tag_name) } + let(:commit) { tag.dereferenced_target } - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: @tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end + context 'annotated tag' do + let(:tag_name) { Gitlab::Git.ref_name(ref) } - context "with commits" do - subject { @push_data[:commits] } + before do + service.execute + @push_data = service.push_data + end - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) + it { is_expected.to include(object_kind: 'tag_push') } + it { is_expected.to include(ref: ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } + it { is_expected.to include(message: tag.message) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } + + context "with repository data" do + subject { @push_data[:repository] } + + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } end - context "the commit" do - subject { @push_data[:commits].first } - - it { is_expected.to include(id: @commit.id) } - it { is_expected.to include(message: @commit.safe_message) } - it { is_expected.to include(timestamp: @commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - @commit.id - ].join('/') - ) + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { @push_data[:commits].first } + + it { is_expected.to include(id: commit.id) } + it { is_expected.to include(message: commit.safe_message) } + it { is_expected.to include(timestamp: commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: commit.author_name) } + it { is_expected.to include(email: commit.author_email) } + end end + end + end - context "with a author" do - subject { @push_data[:commits].first[:author] } + context 'lightweight tag' do + let(:tag_name) { 'light-tag' } + let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } + let(:ref) { "refs/tags/light-tag" } + + before do + # Create the lightweight tag + project.repository.raw_repository.rugged.tags.create(tag_name, newrev) + + # Clear tag list cache + project.repository.expire_tags_cache + + service.execute + @push_data = service.push_data + end + + it { is_expected.to include(object_kind: 'tag_push') } + it { is_expected.to include(ref: ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } + it { is_expected.to include(message: tag.message) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } + + context "with repository data" do + subject { @push_data[:repository] } + + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } + end + + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end - it { is_expected.to include(name: @commit.author_name) } - it { is_expected.to include(email: @commit.author_email) } + context "the commit" do + subject { @push_data[:commits].first } + + it { is_expected.to include(id: commit.id) } + it { is_expected.to include(message: commit.safe_message) } + it { is_expected.to include(timestamp: commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: commit.author_name) } + it { is_expected.to include(email: commit.author_email) } + end end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index cbfc63de811..7a9b34f9f96 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe Labels::FindOrCreateService, services: true do describe '#execute' do - let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, namespace: group) } @@ -14,37 +13,49 @@ describe Labels::FindOrCreateService, services: true do } end - subject(:service) { described_class.new(user, project, params) } - - before do - project.team << [user, :developer] - end + context 'when acting on behalf of a specific user' do + let(:user) { create(:user) } + subject(:service) { described_class.new(user, project, params) } + before do + project.team << [user, :developer] + end - context 'when label does not exist at group level' do - it 'creates a new label at project level' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project level' do + expect { service.execute }.to change(project.labels, :count).by(1) + end end - end - context 'when label exists at group level' do - it 'returns the group label' do - group_label = create(:group_label, group: group, title: 'Security') + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') - expect(service.execute).to eq group_label + expect(service.execute).to eq group_label + end end - end - context 'when label does not exist at group level' do - it 'creates a new label at project leve' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project leve' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end + + context 'when label exists at project level' do + it 'returns the project label' do + project_label = create(:label, project: project, title: 'Security') + + expect(service.execute).to eq project_label + end end end - context 'when label exists at project level' do + context 'when authorization is not required' do + subject(:service) { described_class.new(nil, project, params) } + it 'returns the project label' do project_label = create(:label, project: project, title: 'Security') - expect(service.execute).to eq project_label + expect(service.execute(skip_authorization: true)).to eq project_label end end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index 03e296259f9..7b090343a3e 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do let(:access_requester) { create(:user) } let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } + let(:opts) { {} } shared_examples 'a service raising ActiveRecord::RecordNotFound' do it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service approving an access request' do it 'succeeds' do - expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1) + expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1) end it 'returns a <Source>Member' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params).execute(opts) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_nil end context 'with a custom access level' do - let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } } + let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) } it 'returns a ProjectMember with the custom access level' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params2).execute(opts) expect(member.access_level).to eq Gitlab::Access::MASTER end @@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do end let(:params) { { user_id: access_requester.id } } + context 'when current user is nil' do + let(:user) { nil } + + context 'and :force option is not given' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is false' do + let(:opts) { { force: false } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is true' do + let(:opts) { { force: true } } + + it_behaves_like 'a service approving an access request' do + let(:source) { project } + end + + it_behaves_like 'a service approving an access request' do + let(:source) { group } + end + end + + context 'and :force param is true' do + let(:params) { { user_id: access_requester.id, force: true } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + end + context 'when current user cannot approve access request to the project' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { project } diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb new file mode 100644 index 00000000000..0670ac2faa2 --- /dev/null +++ b/spec/services/members/create_service_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Members::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:project_user) { create(:user) } + + before { project.team << [user, :master] } + + it 'adds user to members' do + params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } + result = described_class.new(project, user, params).execute + + expect(result).to be_truthy + expect(project.users).to include project_user + end + + it 'adds no user to members' do + params = { user_ids: '', access_level: Gitlab::Access::GUEST } + result = described_class.new(project, user, params).execute + + expect(result).to be_falsey + expect(project.users).not_to include project_user + end +end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index b80cfd8f450..1f90efdbd6a 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -147,6 +147,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do expect(MergeWorker).not_to receive(:perform_async) build.success + test.reload test.drop end @@ -154,6 +155,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do expect(MergeWorker).to receive(:perform_async) build.success + test.reload test.success end end diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 5d400299be0..92b84308f73 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -18,7 +18,7 @@ describe Milestones::CloseService, services: true do it { expect(milestone).to be_closed } describe :event do - let(:event) { Event.first } + let(:event) { Event.recent.first } it { expect(event.milestone).to be_truthy } it { expect(event.target).to eq(milestone) } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 3ea1273abc3..876bfaf085c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) + @opts.merge!(wiki_enabled: false) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index ed1384798ab..ab6e8f537ba 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -110,7 +110,7 @@ describe Projects::ImportService, services: true do end it 'expires existence cache after error' do - allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true) + allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true) expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b4ba28dfe8e..5bb107fdd85 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -531,53 +531,47 @@ describe SystemNoteService, services: true do include JiraServiceHelper describe 'JIRA integration' do - let(:project) { create(:jira_project) } - let(:author) { create(:user) } - let(:issue) { create(:issue, project: project) } - let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } - let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} - let(:jira_tracker) { project.jira_service } - let(:commit) { project.repository.commits('master').find { |commit| commit.id == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } } + let(:project) { create(:jira_project) } + let(:author) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } + let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} + let(:jira_tracker) { project.jira_service } + let(:commit) { project.commit } + let(:comment_url) { jira_api_comment_url(jira_issue.id) } + let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." } + + before { stub_jira_urls(jira_issue.id) } context 'in JIRA issue tracker' do - before do - jira_service_settings - WebMock.stub_request(:post, jira_api_comment_url) - end + before { jira_service_settings } describe "new reference" do - before do - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - end - subject { described_class.cross_reference(jira_issue, commit, author) } - it { is_expected.to eq(jira_status_message) } - end - - describe "existing reference" do - before do - message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'} - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]})) - end - - subject { described_class.cross_reference(jira_issue, commit, author) } - it { is_expected.not_to eq(jira_status_message) } + it { is_expected.to eq(success_message) } end end context 'issue from an issue' do context 'in JIRA issue tracker' do - before do - jira_service_settings - WebMock.stub_request(:post, jira_api_comment_url) - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - end + before { jira_service_settings } subject { described_class.cross_reference(jira_issue, issue, author) } - it { is_expected.to eq(jira_status_message) } + it { is_expected.to eq(success_message) } + end + end + + describe "existing reference" do + before do + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" + allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) end + + subject { described_class.cross_reference(jira_issue, commit, author) } + + it { is_expected.not_to eq(success_message) } end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 06d52f0f735..b2ca856f89f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,7 +9,7 @@ require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' -if ENV['CI'] +if ENV['CI'] && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind end diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb new file mode 100644 index 00000000000..eb5da662ab5 --- /dev/null +++ b/spec/support/banzai/reference_filter_shared_examples.rb @@ -0,0 +1,13 @@ +# Specs for reference links containing HTML. +# +# Requires a reference: +# let(:reference) { '#42' } +shared_examples 'a reference containing an element node' do + let(:inner_html) { 'element <code>node</code> inside' } + let(:reference_with_element) { %{<a href="#{reference}">#{inner_html}</a>} } + + it 'does not escape inner html' do + doc = reference_filter(reference_with_element) + expect(doc.children.first.inner_html).to eq(inner_html) + end +end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb new file mode 100644 index 00000000000..adc3f48b434 --- /dev/null +++ b/spec/support/javascript_fixtures_helpers.rb @@ -0,0 +1,45 @@ +require 'fileutils' +require 'gitlab/popen' + +module JavaScriptFixturesHelpers + include Gitlab::Popen + + FIXTURE_PATH = 'spec/javascripts/fixtures' + + # Public: Removes all fixture files from given directory + # + # directory_name - directory of the fixtures (relative to FIXTURE_PATH) + # + def clean_frontend_fixtures(directory_name) + directory_name = File.expand_path(directory_name, FIXTURE_PATH) + Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name| + FileUtils.rm(file_name) + end + end + + # Public: Store a response object as fixture file + # + # response - response object to store + # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH) + # + def store_frontend_fixture(response, fixture_file_name) + fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH) + fixture = response.body + + response_mime_type = Mime::Type.lookup(response.content_type) + if response_mime_type.html? + doc = Nokogiri::HTML::DocumentFragment.parse(fixture) + + scripts = doc.css('script') + scripts.remove + + fixture = doc.to_html + + # replace relative links + fixture.gsub!(%r{="/}, '="https://fixture.invalid/') + end + + FileUtils.mkdir_p(File.dirname(fixture_file_name)) + File.write(fixture_file_name, fixture) + end +end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index f3ea206f387..96e0dad6b55 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -1,20 +1,17 @@ module JiraServiceHelper + JIRA_URL = "http://jira.example.net" + JIRA_API = JIRA_URL + "/rest/api/2" + def jira_service_settings properties = { - "title" => "JIRA tracker", - "project_url" => "http://jira.example/issues/?jql=project=A", - "issues_url" => "http://jira.example/browse/JIRA-1", - "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa", - "api_url" => "http://jira.example/rest/api/2" + title: "JIRA tracker", + url: JIRA_URL, + project_key: "JIRA" } jira_tracker.update_attributes(properties: properties, active: true) end - def jira_status_message - "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}." - end - def jira_issue_comments "{\"startAt\":0,\"maxResults\":11,\"total\":11, \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\", @@ -52,15 +49,32 @@ module JiraServiceHelper ]}" end - def jira_api_comment_url - 'http://jira.example/rest/api/2/issue/JIRA-1/comment' + def jira_project_url + JIRA_API + "/project/#{jira_tracker.project_key}" + end + + def jira_api_comment_url(issue_id) + JIRA_API + "/issue/#{issue_id}/comment" end - def jira_api_transition_url - 'http://jira.example/rest/api/2/issue/JIRA-1/transitions' + def jira_api_transition_url(issue_id) + JIRA_API + "/issue/#{issue_id}/transitions" end def jira_api_test_url - 'http://jira.example/rest/api/2/myself' + JIRA_API + "/myself" + end + + def jira_issue_url(issue_id) + JIRA_API + "/issue/#{issue_id}" + end + + def stub_jira_urls(issue_id) + WebMock.stub_request(:get, jira_project_url) + WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments) + WebMock.stub_request(:get, jira_issue_url(issue_id)) + WebMock.stub_request(:get, jira_api_test_url) + WebMock.stub_request(:post, jira_api_comment_url(issue_id)) + WebMock.stub_request(:post, jira_api_transition_url(issue_id)) end end diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 201614e45a4..ad1c783df4d 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -17,6 +17,8 @@ shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('2 of') expect(subject.task_status).to match('5 tasks completed') + expect(subject.task_status_short).to match('2/') + expect(subject.task_status_short).to match('5 tasks') end describe '#tasks?' do @@ -41,6 +43,8 @@ shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('0 of') expect(subject.task_status).to match('1 task completed') + expect(subject.task_status_short).to match('0/') + expect(subject.task_status_short).to match('1 task') end end @@ -54,6 +58,8 @@ shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('1 of') expect(subject.task_status).to match('1 task completed') + expect(subject.task_status_short).to match('1/') + expect(subject.task_status_short).to match('1 task') end end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 73bc8326f02..287d83344db 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -79,7 +79,7 @@ describe 'gitlab:app namespace rake task' do end end # backup_restore task - describe 'backup_create' do + describe 'backup' do def tars_glob Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) end @@ -98,6 +98,78 @@ describe 'gitlab:app namespace rake task' do @backup_tar = tars_glob.first end + def restore_backup + orig_stdout = $stdout + $stdout = StringIO.new + reenable_backup_sub_tasks + run_rake_task('gitlab:backup:restore') + reenable_backup_sub_tasks + $stdout = orig_stdout + end + + describe 'backup creation and deletion using annex and custom_hooks' do + let(:project) { create(:project) } + let(:user_backup_path) { "repositories/#{project.path_with_namespace}" } + + before(:each) do + @origin_cd = Dir.pwd + + path = File.join(project.repository.path_to_repo, filename) + FileUtils.mkdir_p(path) + FileUtils.touch(File.join(path, "dummy.txt")) + + # We need to use the full path instead of the relative one + allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(File.expand_path(Gitlab.config.gitlab_shell.path, Rails.root.to_s)) + + ENV["SKIP"] = "db" + create_backup + end + + after(:each) do + ENV["SKIP"] = "" + FileUtils.rm(@backup_tar) + Dir.chdir(@origin_cd) + end + + context 'project uses git-annex and successfully creates backup' do + let(:filename) { "annex" } + + it 'creates annex.tar and project bundle' do + tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}}) + + expect(exit_status).to eq(0) + expect(tar_contents).to match(user_backup_path) + expect(tar_contents).to match("#{user_backup_path}/annex.tar") + expect(tar_contents).to match("#{user_backup_path}.bundle") + end + + it 'restores files correctly' do + restore_backup + + expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt") + end + end + + context 'project uses custom_hooks and successfully creates backup' do + let(:filename) { "custom_hooks" } + + it 'creates custom_hooks.tar and project bundle' do + tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}}) + + expect(exit_status).to eq(0) + expect(tar_contents).to match(user_backup_path) + expect(tar_contents).to match("#{user_backup_path}/custom_hooks.tar") + expect(tar_contents).to match("#{user_backup_path}.bundle") + end + + it 'restores files correctly' do + restore_backup + + expect(Dir.entries(File.join(project.repository.path, "custom_hooks"))).to include("dummy.txt") + end + end + end + context 'tar creation' do before do create_backup diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb new file mode 100644 index 00000000000..16bf0698c4b --- /dev/null +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe 'projects/commit/_commit_box.html.haml' do + include Devise::Test::ControllerHelpers + + let(:project) { create(:project) } + + before do + assign(:project, project) + assign(:commit, project.commit) + end + + it 'shows the commit SHA' do + render + + expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}") + end + + it 'shows the last pipeline that ran for the commit' do + create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success') + create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled') + third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed') + + render + + expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed") + end +end diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb index c8a3d02d8fd..889d9a38887 100644 --- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -5,7 +5,7 @@ describe 'projects/issues/_related_branches' do let(:project) { create(:project) } let(:branch) { project.repository.find_branch('feature') } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') } before do assign(:project, project) diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb new file mode 100644 index 00000000000..6d42946de38 --- /dev/null +++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe RemoveUnreferencedLfsObjectsWorker do + let(:worker) { RemoveUnreferencedLfsObjectsWorker.new } + + describe '#perform' do + let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') } + let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') } + let!(:project1) { create(:empty_project, lfs_enabled: true) } + let!(:project2) { create(:empty_project, lfs_enabled: true) } + let!(:referenced_lfs_object1) { create(:lfs_object, oid: '3') } + let!(:referenced_lfs_object2) { create(:lfs_object, oid: '4') } + let!(:lfs_objects_project1_1) do + create(:lfs_objects_project, + project: project1, + lfs_object: referenced_lfs_object1 + ) + end + let!(:lfs_objects_project2_1) do + create(:lfs_objects_project, + project: project2, + lfs_object: referenced_lfs_object1 + ) + end + let!(:lfs_objects_project1_2) do + create(:lfs_objects_project, + project: project1, + lfs_object: referenced_lfs_object2 + ) + end + + it 'removes unreferenced lfs objects' do + worker.perform + + expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty + expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty + end + + it 'leaves referenced lfs objects' do + worker.perform + + expect(referenced_lfs_object1.reload).to be_present + expect(referenced_lfs_object2.reload).to be_present + end + + it 'removes unreferenced lfs objects after project removal' do + project1.destroy + + worker.perform + + expect(referenced_lfs_object1.reload).to be_present + expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty + end + end +end |