diff options
456 files changed, 7372 insertions, 2365 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000000..b676916fdf4 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,44 @@ +### Summary + +(Summarize the bug encountered concisely) + +### Steps to reproduce + +(How one can reproduce the issue - this is very important) + +### Expected behavior + +(What you should see instead) + +### Actual behaviour + +(What actually happens) + +### Relevant logs and/or screenshots + +(Paste any relevant logs - please use code blocks (```) to format console output, +logs, and code as it's very hard to read otherwise.) + +### Output of checks + +#### Results of GitLab application Check + +(For installations with omnibus-gitlab package run and paste the output of: +`sudo gitlab-rake gitlab:check SANITIZE=true`) + +(For installations from source run and paste the output of: +`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`) + +(we will only investigate if the tests are passing) + +#### Results of GitLab environment info + +(For installations with omnibus-gitlab package run and paste the output of: +`sudo gitlab-rake gitlab:env:info`) + +(For installations from source run and paste the output of: +`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) + +### Possible fixes + +(If you can, link to the line of code that might be responsible for the problem) diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md new file mode 100644 index 00000000000..ea895ee6275 --- /dev/null +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -0,0 +1,7 @@ +### Description + +(Include problem, use cases, benefits, and/or goals) + +### Proposal + +### Links / references diff --git a/CHANGELOG b/CHANGELOG index fec7c56c32d..7941a29d4ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,12 +1,97 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.12.0 (unreleased) + - Make push events have equal vertical spacing. + - Add two-factor recovery endpoint to internal API !5510 + - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) + - Add font color contrast to external label in admin area (ClemMakesApps) + - Change logo animation to CSS (ClemMakesApps) + - Change merge_error column from string to text type + - Reduce contributions calendar data payload (ClemMakesApps) + - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) + - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) + - Shorten task status phrase (ClemMakesApps) + - Add hover color to emoji icon (ClemMakesApps) + - Fix branches page dropdown sort alignment (ClemMakesApps) + - Add white background for no readme container (ClemMakesApps) + - Optimistic locking for Issues and Merge Requests (title and description overriding prevention) + - Add `wiki_page_events` to project hook APIs (Ben Boeckel) + - Remove Gitorious import + - Fix inconsistent background color for filter input field (ClemMakesApps) + - Add Sentry logging to API calls + - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) + - Remove unused mixins (ClemMakesApps) + - Add search to all issue board lists + - Fix groups sort dropdown alignment (ClemMakesApps) + - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps) + - Fix markdown help references (ClemMakesApps) + - Add last commit time to repo view (ClemMakesApps) + - Added project specific enable/disable setting for LFS !5997 + - Added tests for diff notes + - Add a button to download latest successful artifacts for branches and tags !5142 + - Remove redundant pipeline tooltips (ClemMakesApps) + - Add delimiter to project stars and forks count (ClemMakesApps) + - Fix badge count alignment (ClemMakesApps) + - Fix branch title trailing space on hover (ClemMakesApps) + - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) + - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) + - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) + - Update merge_requests.md with a simpler way to check out a merge request. !5944 + - Fix button missing type (ClemMakesApps) + - Move to project dropdown with infinite scroll for better performance + - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz) + - Load branches asynchronously in Cherry Pick and Revert dialogs. + - Add merge request versions !5467 + - Change using size to use count and caching it for number of group members. !5935 + - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck) + - Reduce number of database queries on builds tab + - Capitalize mentioned issue timeline notes (ClemMakesApps) + - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) + - Adds response mime type to transaction metric action when it's not HTML + - Fix hover leading space bug in pipeline graph !5980 + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 + +v 8.11.4 (unreleased) + - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) + - Creating an issue through our API now emails label subscribers !5720 + - Fix resolving conflicts on forks + - Fix diff commenting on merge requests created prior to 8.10 + +v 8.11.4 (unreleased) + - Fix issue boards leak private label names and descriptions + +v 8.11.3 (unreleased) +v 8.11.3 + - Do not enforce using hash with hidden key in CI configuration. !6079 + - Allow system info page to handle case where info is unavailable + - Label list shows all issues (opened or closed) with that label + - Don't show resolve conflicts link before MR status is updated + - Fix "Wiki" link not appearing in navigation for projects with external wiki + - Fix IE11 fork button bug !598 + - Don't prevent viewing the MR when git refs for conflicts can't be found on disk + - Fix external issue tracker "Issues" link leading to 404s + +v 8.11.2 + - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 + - Use gitlab-workhorse 0.7.11 !5983 + - Does not halt the GitHub import process when an error occurs. !5763 + - Fix file links on project page when default view is Files !5933 + - Fixed enter key in search input not working !5888 + +v 8.11.1 + - Pulled due to packaging error. + v 8.11.0 (unreleased) + - Fix pipelines tab layout regression (brycepj) +v 8.11.0 + - Use test coverage value from the latest successful pipeline in badge. !5862 - Add test coverage report badge. !5708 - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) + - Add Koding (online IDE) integration - Ability to specify branches for Pivotal Tracker integration (Egor Lynko) - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - - Add delimiter to project stars and forks count (ClemMakesApps) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) + - Fix adding line comments on the initial commit to a repo !5900 - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz) - Update to Ruby 2.3.1. !4948 @@ -18,8 +103,8 @@ v 8.11.0 (unreleased) - API: Endpoints for enabling and disabling deploy keys - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833 - Use long options for curl examples in documentation !5703 (winniehell) + - Added tooltip listing label names to the labels value in the collapsed issuable sidebar - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - - Fix badge count alignment (ClemMakesApps) - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell) - Allow naming U2F devices !5833 @@ -27,6 +112,7 @@ v 8.11.0 (unreleased) - Fix CI status icon link underline (ClemMakesApps) - The Repository class is now instrumented - Fix commit mention font inconsistency (ClemMakesApps) + - Do not escape URI when extracting path !5878 (winniehell) - Fix filter label tooltip HTML rendering (ClemMakesApps) - Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Expand commit message width in repo view (ClemMakesApps) @@ -36,9 +122,11 @@ v 8.11.0 (unreleased) - API: Add deployment endpoints - API: Add Play endpoint on Builds - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' + - Show wall clock time when showing a pipeline. !5734 - Show member roles to all users on members page - Project.visible_to_user is instrumented again - Fix awardable button mutuality loading spinners (ClemMakesApps) + - Sort todos by date and priority - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Optimize maximum user access level lookup in loading of notes - Send notification emails to users newly mentioned in issue and MR edits !5800 @@ -57,7 +145,6 @@ v 8.11.0 (unreleased) - Enforce 2FA restrictions on API authentication endpoints !5820 - Limit git rev-list output count to one in forced push check - Show deployment status on merge requests with external URLs - - Fix branch title trailing space on hover (ClemMakesApps) - Clean up unused routes (Josef Strzibny) - Fix issue on empty project to allow developers to only push to protected branches if given permission - API: Add enpoints for pipelines @@ -82,7 +169,6 @@ v 8.11.0 (unreleased) - Add archived badge to project list !5798 - Add simple identifier to public SSH keys (muteor) - Admin page now references docs instead of a specific file !5600 (AnAverageHuman) - - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Fix filter input alignment (ClemMakesApps) - Include old revision in merge request update hooks (Ben Boeckel) - Add build event color in HipChat messages (David Eisner) @@ -95,8 +181,6 @@ v 8.11.0 (unreleased) - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` - Add pipeline events hook - - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) - - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) - Bump gitlab_git to speedup DiffCollection iterations - Rewrite description of a blocked user in admin settings. (Elias Werberich) - Make branches sortable without push permission !5462 (winniehell) @@ -108,14 +192,12 @@ v 8.11.0 (unreleased) - Fix search for notes which belongs to deleted objects - Allow Akismet to be trained by submitting issues as spam or ham !5538 - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps) - Allow branch names ending with .json for graph and network page !5579 (winniehell) - Add the `sprockets-es6` gem - Improve OAuth2 client documentation (muteor) - Fix diff comments inverted toggle bug (ClemMakesApps) - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) - Profile requests when a header is passed - - Fix button missing type (ClemMakesApps) - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab. - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible - Add commit stats in commit api. !5517 (dixpac) @@ -129,6 +211,7 @@ v 8.11.0 (unreleased) - Change requests_profiles resource constraint to catch virtually any file - Bump gitlab_git to lazy load compare commits - Reduce number of queries made for merge_requests/:id/diffs + - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski) - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y) - Fix bug where destroying a namespace would not always destroy projects - Fix RequestProfiler::Middleware error when code is reloaded in development @@ -155,6 +238,10 @@ v 8.11.0 (unreleased) - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +v 8.10.7 + - Upgrade Hamlit to 2.6.1. !5873 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.10.6 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Restore "Largest repository" sort option on Admin > Projects page. !5797 @@ -183,8 +270,6 @@ v 8.10.3 - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588 - Fix label already exist error message in the right sidebar. -v 8.10.3 (unreleased) - v 8.10.2 - User can now search branches by name. !5144 - Page is now properly rendered after committing the first file and creating the first branch. !5399 @@ -378,6 +463,9 @@ v 8.10.0 - Fix migration corrupting import data for old version upgrades - Show tooltip on GitLab export link in new project page +v 8.9.8 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.9.7 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Require administrator privileges to perform a project import. @@ -647,6 +735,9 @@ v 8.9.0 - Add tooltip to pin/unpin navbar - Add new sub nav style to Wiki and Graphs sub navigation +v 8.8.9 + - Upgrade Doorkeeper to 4.2.0. !5881 + v 8.8.8 - Upgrade Rails to 4.2.7.1 for security fixes. !5781 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f10f90a82d..c77dcd96a7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,7 +129,7 @@ request that potentially fixes it. ### Feature proposals -To create a feature proposal for CE and CI, open an issue on the +To create a feature proposal for CE, open an issue on the [issue tracker of CE][ce-tracker]. For feature proposals for EE, open an issue on the @@ -144,16 +144,7 @@ code snippet right after your description in a new line: `~"feature proposal"`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. -You are encouraged to use the template below for feature proposals. - -``` -## Description -Include problem, use cases, benefits, and/or goals - -## Proposal - -## Links / references -``` +Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker. For changes in the interface, it can be helpful to create a mockup first. If you want to create something yourself, consider opening an issue first to @@ -166,55 +157,11 @@ submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and/or join the discussion. -Please submit bugs using the following template in the issue description area. +Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker. The text in the parenthesis is there to help you with what to include. Omit it when submitting the actual issue. You can copy-paste it and then edit as you see fit. -``` -## Summary - -(Summarize your issue in one sentence - what goes wrong, what did you expect to happen) - -## Steps to reproduce - -(How one can reproduce the issue - this is very important) - -## Expected behavior - -(What you should see instead) - -## Relevant logs and/or screenshots - -(Paste any relevant logs - please use code blocks (```) to format console output, -logs, and code as it's very hard to read otherwise.) - -## Output of checks - -### Results of GitLab Application Check - -(For installations with omnibus-gitlab package run and paste the output of: -sudo gitlab-rake gitlab:check SANITIZE=true) - -(For installations from source run and paste the output of: -sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true) - -(we will only investigate if the tests are passing) - -### Results of GitLab Environment Info - -(For installations with omnibus-gitlab package run and paste the output of: -sudo gitlab-rake gitlab:env:info) - -(For installations from source run and paste the output of: -sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production) - -## Possible fixes - -(If you can, link to the line of code that might be responsible for the problem) - -``` - ### Issue weight Issue weight allows us to get an idea of the amount of work required to solve @@ -389,7 +336,8 @@ description area. Copy-paste it to retain the markdown format. 1. The change is as small as possible 1. Include proper tests and make all tests pass (unless it contains a test - exposing a bug in existing code) + exposing a bug in existing code). Every new class should have corresponding + unit tests, even if the class is exercised at a higher level, such as a feature test. 1. If you suspect a failing CI build is unrelated to your contribution, you may try and restart the failing CI job or ask a developer to fix the aforementioned failing test diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 619b5376684..18091983f59 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.3.3 +3.4.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index e7c7d3cc3c8..b4d6d12101f 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.8 +0.7.11 @@ -349,5 +349,5 @@ gem 'paranoia', '~> 2.0' gem 'health_check', '~> 2.1.0' # System information -gem 'vmstat', '~> 2.1.1' +gem 'vmstat', '~> 2.2' gem 'sys-filesystem', '~> 1.1.6' diff --git a/Gemfile.lock b/Gemfile.lock index 5511d718938..0c28975060c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -772,7 +772,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - vmstat (2.1.1) + vmstat (2.2.0) warden (1.2.6) rack (>= 1.0) web-console (2.3.0) @@ -980,7 +980,7 @@ DEPENDENCIES unicorn-worker-killer (~> 0.4.2) version_sorter (~> 2.1.0) virtus (~> 1.0.1) - vmstat (~> 2.1.1) + vmstat (~> 2.2) web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) diff --git a/README.md b/README.md index fee93d5f9c3..3df8bfa04c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # GitLab [![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) +[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) ## Canonical source @@ -69,7 +70,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL -- Ruby (MRI) 2.1 +- Ruby (MRI) 2.3 - Git 2.7.4+ - Redis 2.8+ - MySQL or PostgreSQL @@ -1 +1 @@ -8.11.0-pre +8.12.0-pre diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg new file mode 100644 index 00000000000..ad89d684d94 --- /dev/null +++ b/app/assets/images/koding-logo.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14"> + <g fill="#d6d7d9"> + <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/> + <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/> + <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/> + </g> +</svg>
\ No newline at end of file diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 index 748084b0307..2fe46b9fd06 100644 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ b/app/assets/javascripts/abuse_reports.js.es6 @@ -1,4 +1,3 @@ -window.gl = window.gl || {}; ((global) => { const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; @@ -36,4 +35,4 @@ window.gl = window.gl || {}; } global.AbuseReports = AbuseReports; -})(window.gl); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 1ab3c2197d8..5ea6086ab77 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -26,7 +26,7 @@ event_filters = $.cookie("event_filter"); filter = sender.attr("id").split("_")[0]; $.cookie("event_filter", (event_filters !== filter ? filter : ""), { - path: '/' + path: gon.relative_url_root || '/' }); if (event_filters !== filter) { return sender.closest('li').toggleClass("active"); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a122fa2d637..43a679501a7 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -26,8 +26,6 @@ /*= require bootstrap/tooltip */ /*= require bootstrap/popover */ /*= require select2 */ -/*= require ace-rails-ap */ -/*= require ace/ext-searchbox */ /*= require underscore */ /*= require dropzone */ /*= require mousetrap */ @@ -153,7 +151,9 @@ }); }); $('.remove-row').bind('ajax:success', function() { - return $(this).closest('li').fadeOut(); + $(this).tooltip('destroy') + .closest('li') + .fadeOut(); }); $('.js-remove-tr').bind('ajax:before', function() { return $(this).hide(); @@ -288,7 +288,7 @@ new Aside(); if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') { $.cookie('pin_nav', 'false', { - path: '/', + path: gon.relative_url_root || '/', expires: 365 * 10 }); $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned'); @@ -313,7 +313,7 @@ $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded'); } $.cookie('pin_nav', doPinNav, { - path: '/', + path: gon.relative_url_root || '/', expires: 365 * 10 }); if ($.cookie('pin_nav') === 'true' || doPinNav) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index aee1c29eee3..ad12cb906e1 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -320,6 +320,7 @@ frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis.push(emoji); return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { + path: gon.relative_url_root || '/', expires: 365 }); }; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 1b7b63489ea..8ac1ba7665e 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,10 +1,26 @@ -(function() { +(function(w) { $(function() { - return $("body").on("click", ".js-toggle-button", function(e) { - $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up'); - $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle(); - return e.preventDefault(); + $('.js-toggle-button').on('click', function(e) { + e.preventDefault(); + $(this) + .find('.fa') + .toggleClass('fa-chevron-down fa-chevron-up') + .end() + .closest('.js-toggle-container') + .find('.js-toggle-content') + .toggle() + ; }); - }); -}).call(this); + // If we're accessing a permalink, ensure it is not inside a + // closed js-toggle-container! + var hash = w.gl.utils.getLocationHash(); + var anchor = hash && document.getElementById(hash); + var container = anchor && $(anchor).closest('.js-toggle-container'); + + if (container && container.find('.js-toggle-content').is(':hidden')) { + container.find('.js-toggle-button').trigger('click'); + anchor.scrollIntoView(); + } + }); +})(window); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js new file mode 100644 index 00000000000..2afef43f3d6 --- /dev/null +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -0,0 +1,12 @@ +/*= require_tree . */ + +(function() { + $(function() { + var url = $(".js-edit-blob-form").data("relative-url-root"); + url += $(".js-edit-blob-form").data("assets-prefix"); + + var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); + new NewCommitForm($('.js-edit-blob-form')); + }); + +}).call(this); diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 649c79daee8..649c79daee8 100644 --- a/app/assets/javascripts/blob/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 2c65d4427be..91c12570e09 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -38,7 +38,7 @@ $(() => { ready () { Store.disabled = this.disabled; gl.boardService.all() - .then((resp) => { + .then((resp) => { resp.json().forEach((board) => { const list = Store.addList(board); @@ -54,4 +54,11 @@ $(() => { }); } }); + + gl.IssueBoardsSearch = new Vue({ + el: '#js-boards-seach', + data: { + filters: Store.state.filters + } + }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index e17784e7948..7e86f001f44 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,15 +21,10 @@ }, data () { return { - query: '', filters: Store.state.filters }; }, watch: { - query () { - this.list.filters = this.getFilterData(); - this.list.getIssues(true); - }, filters: { handler () { this.list.page = 1; @@ -38,16 +33,6 @@ deep: true } }, - methods: { - getFilterData () { - const filters = this.filters; - let queryData = { search: this.query }; - - Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; }); - - return queryData; - } - }, ready () { const options = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, @@ -55,7 +40,7 @@ draggable: '.is-draggable', handle: '.js-board-handle', onEnd: (e) => { - document.body.classList.remove('is-dragging'); + gl.issueBoards.onEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { const order = this.sortable.toArray(), @@ -72,10 +57,6 @@ } }); - if (bp.getBreakpointSize() === 'xs') { - options.handle = '.js-board-drag-handle'; - } - this.sortable = Sortable.create(this.$el.parentNode, options); }, beforeDestroy () { diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 1503d14c508..a6644e9eb8c 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -63,6 +63,8 @@ Store.moving.issue = card.issue; Store.moving.list = card.list; + + gl.issueBoards.onStart(); }, onAdd: (e) => { gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); @@ -72,10 +74,6 @@ } }); - if (bp.getBreakpointSize() === 'xs') { - options.handle = '.js-card-drag-handle'; - } - this.sortable = Sortable.create(this.$els.list, options); // Scroll event on list to load more 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 b7afe4897b6..44addb3ea98 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -2,6 +2,19 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; + gl.issueBoards.onStart = () => { + $('.has-tooltip').tooltip('hide') + .tooltip('disable'); + document.body.classList.add('is-dragging'); + }; + + gl.issueBoards.onEnd = () => { + $('.has-tooltip').tooltip('enable'); + document.body.classList.remove('is-dragging'); + }; + + gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; + gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { let defaultSortOptions = { forceFallback: true, @@ -9,14 +22,11 @@ fallbackOnBody: true, ghostClass: 'is-ghost', filter: '.has-tooltip', - scrollSensitivity: 100, + delay: gl.issueBoards.touchEnabled ? 100 : 0, + scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, - onStart () { - document.body.classList.add('is-dragging'); - }, - onEnd () { - document.body.classList.remove('is-dragging'); - } + onStart: gl.issueBoards.onStart, + onEnd: gl.issueBoards.onEnd } Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6 index e81e91fe972..583829552cd 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js.es6 @@ -3,6 +3,7 @@ class ListLabel { this.id = obj.id; this.title = obj.title; this.color = obj.color; + this.textColor = obj.text_color; this.description = obj.description; this.priority = (obj.priority !== null) ? obj.priority : Infinity; } diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index be2b8c568a8..816fa49516c 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -58,10 +58,6 @@ class List { } } - canSearch () { - return this.type === 'backlog'; - } - getIssues (emptyIssues = true) { const filters = this.filters; let data = { page: this.page }; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 18f26a1f911..bd07ee0c161 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -15,7 +15,8 @@ author_id: gl.utils.getParameterValues('author_id')[0], assignee_id: gl.utils.getParameterValues('assignee_id')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]') + label_name: gl.utils.getParameterValues('label_name[]'), + search: '' }; }, addList (listObj) { diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js index 75f8b730195..75f8b730195 100755..100644 --- a/app/assets/javascripts/boards/test_utils/simulate_drag.js +++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 74c4ab563f9..ba64d2bcf0b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -20,6 +20,9 @@ path = page.split(':'); shortcut_handler = null; switch (page) { + case 'projects:boards:show': + shortcut_handler = new ShortcutsNavigation(); + break; case 'projects:issues:index': Issuable.init(); new IssuableBulkActions(); @@ -126,10 +129,12 @@ new NotificationsDropdown(); break; case 'groups:group_members:index': + new gl.MemberExpirationDate(); new GroupMembers(); new UsersSelect(); break; case 'projects:project_members:index': + new gl.MemberExpirationDate(); new ProjectMembers(); new UsersSelect(); break; @@ -171,6 +176,7 @@ new BuildArtifacts(); break; case 'projects:group_links:index': + new gl.MemberExpirationDate(); new GroupsSelect(); break; case 'search:show': diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index b2e49b71fec..3fb3b1a8b51 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -39,12 +39,13 @@ FilesCommentButton.prototype.render = function(e) { var $currentTarget, buttonParentElement, lineContentElement, textFileElement; $currentTarget = $(e.currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); - if (!this.shouldRender(e, buttonParentElement)) { - return; - } - textFileElement = this.getTextFileElement($currentTarget); + if (!this.validateButtonParent(buttonParentElement)) return; lineContentElement = this.getLineContent($currentTarget); + if (!this.validateLineContent(lineContentElement)) return; + + textFileElement = this.getTextFileElement($currentTarget); buttonParentElement.append(this.buildButton({ noteableType: textFileElement.attr('data-noteable-type'), noteableID: textFileElement.attr('data-noteable-id'), @@ -119,10 +120,14 @@ return newButtonParent.is(this.getButtonParent($(e.currentTarget))); }; - FilesCommentButton.prototype.shouldRender = function(e, buttonParentElement) { + FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; }; + FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { + return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; + }; + return FilesCommentButton; })(); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d3394fae3f9..5a2a8523d9f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -31,8 +31,7 @@ this.input .on('keydown', function (e) { var keyCode = e.which; - - if (keyCode === 13) { + if (keyCode === 13 && !options.elIsInput) { e.preventDefault() } }) @@ -47,7 +46,7 @@ } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.removeClass(HAS_VALUE_CLASS); } - if (keyCode === 13) { + if (keyCode === 13 && !options.elIsInput) { return false; } if (this.options.remote) { @@ -111,14 +110,14 @@ matches = fuzzaldrinPlus.match($el.text().trim(), search_text); if (!$el.is('.dropdown-header')) { if (matches.length) { - return $el.show(); + return $el.show().removeClass('option-hidden'); } else { - return $el.hide(); + return $el.hide().addClass('option-hidden'); } } }); } else { - return elements.show(); + return elements.show().removeClass('option-hidden'); } } }; @@ -179,7 +178,7 @@ })(); GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex; + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex; LOADING_CLASS = "is-loading"; @@ -191,6 +190,12 @@ currentIndex = -1; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; + + CURSOR_SELECT_SCROLL_PADDING = 5 + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; function GitLabDropdown(el1, options) { @@ -213,6 +218,7 @@ if (this.options.data) { if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { this.fullData = this.options.data; + currentIndex = -1; this.parseData(this.options.data); } else { this.remote = new GitLabDropdownRemote(this.options.data, { @@ -232,6 +238,7 @@ } if (this.options.filterable) { this.filter = new GitLabDropdownFilter(this.filterInput, { + elIsInput: $(this.el).is('input'), filterInputBlur: this.filterInputBlur, filterByText: this.options.filterByText, onFilter: this.options.onFilter, @@ -240,7 +247,7 @@ keys: searchFields, elements: (function(_this) { return function() { - selector = '.dropdown-content li:not(.divider)'; + selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = ".dropdown-page-one " + selector; } @@ -256,12 +263,16 @@ return function(data) { _this.parseData(data); if (_this.filterInput.val() !== '') { - selector = '.dropdown-content li:not(.divider):visible'; + selector = SELECTABLE_CLASSES; if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = ".dropdown-page-one " + selector; } - $(selector, _this.dropdown).first().find('a').addClass('is-focused'); - return currentIndex = 0; + if ($(_this.el).is('input')) { + currentIndex = -1; + } else { + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + currentIndex = 0; + } } }; })(this) @@ -376,7 +387,7 @@ var $target; if (this.options.multiSelect) { $target = $(e.target); - if (!$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 { @@ -387,7 +398,7 @@ GitLabDropdown.prototype.opened = function() { var contentHtml; - currentIndex = -1; + this.resetRows(); this.addArrowKeyEvent(); if (this.options.setIndeterminateIds) { this.options.setIndeterminateIds.call(this); @@ -410,6 +421,7 @@ GitLabDropdown.prototype.hidden = function(e) { var $input; + this.resetRows(); this.removeArrayKeyEvent(); $input = this.dropdown.find(".dropdown-input-field"); if (this.options.filterable) { @@ -463,14 +475,15 @@ return "<li class='separator'></li>"; } if (data.header != null) { - return "<li class='dropdown-header'>" + data.header + "</li>"; + return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header }); } if (this.options.renderRow) { html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { value = this.options.id ? this.options.id(data) : data.id; - fieldName = this.options.fieldName; + fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName; + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); if (field.length) { selected = true; @@ -494,11 +507,16 @@ text = this.highlightTextMatches(text, this.filterInput.val()); } if (group) { - groupAttrs = "data-group='" + group + "' data-index='" + index + "'"; + groupAttrs = 'data-group=' + group + ' data-index=' + index; } else { groupAttrs = ''; } - html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>"; + html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({ + url: url, + groupAttrs: groupAttrs, + cssClass: cssClass, + text: text + }); } return html; }; @@ -520,20 +538,8 @@ return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; }; - GitLabDropdown.prototype.highlightRow = function(index) { - var selector; - if (this.filterInput.val() !== "") { - selector = '.dropdown-content li:first-child a'; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content li:first-child a"; - } - return this.getElement(selector).addClass('is-focused'); - } - }; - GitLabDropdown.prototype.rowClicked = function(el) { var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; - fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { groupName = el.data('group'); @@ -545,6 +551,7 @@ selectedObject = this.renderedData[selectedIndex]; } } + fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; if (isInput) { field = $(this.el); @@ -558,11 +565,6 @@ } else { field.remove(); } - if (this.options.toggleLabel) { - return this.updateLabel(selectedObject, el, this); - } else { - return selectedObject; - } } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); @@ -570,9 +572,8 @@ field.remove(); } if (!field.length && fieldName) { - this.addInput(fieldName, value); + this.addInput(fieldName, value, selectedObject); } - return selectedObject; } else { if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); @@ -584,38 +585,54 @@ field.remove(); } el.addClass(ACTIVE_CLASS); - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } if (value != null) { if (!field.length && fieldName) { - this.addInput(fieldName, value); + this.addInput(fieldName, value, selectedObject); } else { field.val(value).trigger('change'); } } - return selectedObject; } + + // Update label right after input has been added + if (this.options.toggleLabel) { + this.updateLabel(selectedObject, el, this); + } + + return selectedObject; }; - GitLabDropdown.prototype.addInput = function(fieldName, value) { + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { var $input; $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); if (this.options.inputId != null) { $input.attr('id', this.options.inputId); } + if (selectedObject && selectedObject.type) { + $input.attr('data-type', selectedObject.type); + } return this.dropdown.before($input); }; GitLabDropdown.prototype.selectRowAtIndex = function(index) { var $el, selector; - selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; + // If we pass an option index + if (typeof index !== "undefined") { + selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; + } else { + selector = ".dropdown-content .is-focused"; + } if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } $el = $(selector, this.dropdown); if ($el.length) { - return $el.first().trigger('click'); + var href = $el.attr('href'); + if (href && href !== '#') { + Turbolinks.visit(href); + } else { + $el.first().trigger('click'); + } } }; @@ -623,7 +640,7 @@ var $input, ARROW_KEY_CODES, selector; ARROW_KEY_CODES = [38, 40]; $input = this.dropdown.find(".dropdown-input-field"); - selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible'; + selector = SELECTABLE_CLASSES; if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } @@ -651,7 +668,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { - return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1); + _this.selectRowAtIndex(); } }; })(this)); @@ -661,6 +678,11 @@ return $('body').off('keydown'); }; + GitLabDropdown.prototype.resetRows = function resetRows() { + currentIndex = -1; + $('.is-focused', this.dropdown).removeClass('is-focused'); + }; + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; $('.is-focused', this.dropdown).removeClass('is-focused'); @@ -674,10 +696,14 @@ listItemHeight = $listItem.outerHeight(); listItemTop = $listItem.prop('offsetTop'); listItemBottom = listItemTop + listItemHeight; - if (listItemBottom > dropdownContentBottom + dropdownScrollTop) { - return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom); - } else if (listItemTop < dropdownContentTop + dropdownScrollTop) { - return $dropdownContent.scrollTop(listItemTop - dropdownContentTop); + if (!index) { + $dropdownContent.scrollTop(0) + } else if (index === ($listItems.length - 1)) { + $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); + } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); + } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); } }; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 297d4f029f0..b7f92ae9883 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -102,20 +102,34 @@ }; IssuableForm.prototype.initMoveDropdown = function() { - var $moveDropdown; + var $moveDropdown, pageSize; $moveDropdown = $('.js-move-dropdown'); if ($moveDropdown.length) { + pageSize = $moveDropdown.data('page-size'); return $('.js-move-dropdown').select2({ ajax: { url: $moveDropdown.data('projects-url'), - results: function(data) { + quietMillis: 125, + data: function(term, page, context) { return { - results: data + search: term, + offset_id: context }; }, - data: function(query) { + results: function(data) { + var context, + more; + + if (data.length >= pageSize) + more = true; + + if (data[data.length - 1]) + context = data[data.length - 1].id; + return { - search: query + results: data, + more: more, + context: context }; } }, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 0526430989f..565dbeacdb3 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); labelUrl = $dropdown.data('labels'); @@ -21,6 +21,7 @@ $block = $selectbox.closest('.block'); $form = $dropdown.closest('form'); $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); + $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); if (issueUpdateURL != null) { @@ -31,7 +32,11 @@ labelNoneHTMLTemplate = '<span class="no-value">None</span>'; } - new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); + $sidebarLabelTooltip.tooltip(); + + if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { + new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId); + } saveLabelData = function() { var data, selected; @@ -52,7 +57,7 @@ dataType: 'JSON', data: data }).done(function(data) { - var labelCount, template; + var labelCount, template, labelTooltipTitle, labelTitles; $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); @@ -66,6 +71,27 @@ } $value.removeAttr('style').html(template); $sidebarCollapsedValue.text(labelCount); + + if (data.labels.length) { + labelTitles = data.labels.map(function(label) { + return label.title; + }); + + if (labelTitles.length > 5) { + labelTitles = labelTitles.slice(0, 5); + labelTitles.push('and ' + (data.labels.length - 5) + ' more'); + } + + labelTooltipTitle = labelTitles.join(', '); + } else { + labelTooltipTitle = ''; + $sidebarLabelTooltip.tooltip('destroy'); + } + + $sidebarLabelTooltip + .attr('title', labelTooltipTitle) + .tooltip('fixTitle'); + $('.has-tooltip', $value).tooltip({ container: 'body' }); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js new file mode 100644 index 00000000000..4cdf99cae72 --- /dev/null +++ b/app/assets/javascripts/lib/ace.js @@ -0,0 +1,2 @@ +/*= require ace-rails-ap */ +/*= require ace/ext-searchbox */ diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 10afa7e4329..d4d5927d3b0 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -67,6 +67,14 @@ $.timeago.settings.strings = tmpLocale; }; + w.gl.utils.getDayDifference = function(a, b) { + var millisecondsPerDay = 1000 * 60 * 60 * 24; + var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return Math.floor((date2 - date1) / millisecondsPerDay); + } + })(window); }).call(this); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index fffbfd19745..533310cc87c 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -43,7 +43,7 @@ } return newUrl; }; - return w.gl.utils.removeParamQueryString = function(url, param) { + w.gl.utils.removeParamQueryString = function(url, param) { var urlVariables, variables; url = decodeURIComponent(url); urlVariables = url.split('&'); @@ -59,6 +59,16 @@ return results; })()).join('&'); }; + w.gl.utils.getLocationHash = function(url) { + var hashIndex; + if (typeof url === 'undefined') { + // Note: We can't use window.location.hash here because it's + // not consistent across browsers - Firefox will pre-decode it + url = window.location.href; + } + hashIndex = url.indexOf('#'); + return hashIndex === -1 ? null : url.substring(hashIndex + 1); + }; })(window); }).call(this); diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 218f24fe908..e5d4fd44c96 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,50 +1,12 @@ (function() { - var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work; - Turbolinks.enableProgressBar(); - defaultClass = 'tanuki-shape'; - - pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek']; - - pieceIndex = 0; - - firstPiece = pieces[0]; - - currentTimer = null; - - delay = 150; - - clearHighlights = function() { - return $("." + defaultClass + ".highlight").attr('class', defaultClass); - }; - start = function() { - clearHighlights(); - pieceIndex = 0; - if (pieces[0] !== firstPiece) { - pieces.reverse(); - } - if (currentTimer) { - clearInterval(currentTimer); - } - return currentTimer = setInterval(work, delay); + $('.tanuki-logo').addClass('animate'); }; stop = function() { - clearInterval(currentTimer); - return clearHighlights(); - }; - - work = function() { - clearHighlights(); - $(pieces[pieceIndex]).attr('class', defaultClass + " highlight"); - if (pieceIndex === pieces.length - 1) { - pieceIndex = 0; - return pieces.reverse(); - } else { - return pieceIndex++; - } + $('.tanuki-logo').removeClass('animate'); }; $(document).on('page:fetch', start); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js new file mode 100644 index 00000000000..1935af491f7 --- /dev/null +++ b/app/assets/javascripts/member_expiration_date.js @@ -0,0 +1,32 @@ +(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 + // `js-clear-input` element, then show that element when there is a value in the + // datepicker, and make clicking on that element clear the field. + // + gl.MemberExpirationDate = function() { + function toggleClearInput() { + $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); + } + + var inputs = $('.js-access-expiration-date'); + + inputs.datepicker({ + dateFormat: 'yy-mm-dd', + minDate: 1, + onSelect: toggleClearInput + }); + + inputs.next('.js-clear-input').on('click', function(event) { + event.preventDefault(); + + var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); + input.datepicker('setDate', null); + toggleClearInput.call(input); + }); + + inputs.on('blur', toggleClearInput); + + inputs.each(toggleClearInput); + }; +}).call(this); diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 index 77bffbcb403..b56fd5aa658 100644 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ b/app/assets/javascripts/merge_conflict_resolver.js.es6 @@ -75,10 +75,8 @@ class MergeConflictResolver { window.location.href = data.redirect_to; }) .error(() => { - new Flash('Something went wrong!'); - }) - .always(() => { this.vue.isSubmitting = false; + new Flash('Something went wrong!'); }); } diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index b97f6d22715..66e097c0a28 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -17,19 +17,15 @@ return $(this).parents('form').submit(); }); $('.hide-no-ssh-message').on('click', function(e) { - var path; - path = '/'; $.cookie('hide_no_ssh_message', 'false', { - path: path + path: gon.relative_url_root || '/' }); $(this).parents('.no-ssh-key-message').remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { - var path; - path = '/'; $.cookie('hide_no_password_message', 'false', { - path: path + path: gon.relative_url_root || '/' }); $(this).parents('.no-password-message').remove(); return e.preventDefault(); @@ -65,7 +61,8 @@ url: $dropdown.data('refs-url'), data: { ref: $dropdown.data('ref') - } + }, + dataType: "json" }).done(function(refs) { return callback(refs); }); @@ -73,7 +70,7 @@ selectable: true, filterable: true, filterByText: true, - fieldName: 'ref', + fieldName: $dropdown.data('field-name'), renderRow: function(ref) { var link; if (ref.header != null) { diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js index f6a796b325a..78f7b48bc7d 100644 --- a/app/assets/javascripts/project_members.js +++ b/app/assets/javascripts/project_members.js @@ -5,9 +5,6 @@ return $(this).fadeOut(); }); } - return ProjectMembers; - })(); - }).call(this); diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 index 2fbb088fa04..7aeb5f92514 100644 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -10,8 +10,12 @@ selectable: true, inputId: $dropdown.data('input-id'), fieldName: $dropdown.data('field-name'), - toggleLabel(item) { - return item.text; + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; + } else { + return 'Select'; + } }, clicked(item, $el, e) { e.preventDefault(); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 2efca2414dc..46beca469b9 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -47,9 +47,7 @@ const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); - if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('input[type="submit"]').removeAttr('disabled'); - } + this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); } } diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index a59fcbfa082..40bc4adb71b 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -31,6 +31,9 @@ const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + // Do not update if one dropdown has not selected any option + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + $.ajax({ type: 'POST', url: this.$wrap.data('url'), diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index dc4d5113826..e3d5f413c77 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -30,7 +30,7 @@ } if (!triggered) { return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), { - path: '/' + path: gon.relative_url_root || '/' }); } }); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 990f6536eb2..227e8c696b4 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -7,7 +7,9 @@ KEYCODE = { ESCAPE: 27, BACKSPACE: 8, - ENTER: 13 + ENTER: 13, + UP: 38, + DOWN: 40 }; function SearchAutocomplete(opts) { @@ -223,6 +225,12 @@ case KEYCODE.ESCAPE: this.restoreOriginalState(); break; + case KEYCODE.ENTER: + this.disableAutocomplete(); + break; + case KEYCODE.UP: + case KEYCODE.DOWN: + return; default: if (this.searchInput.val() === '') { this.disableAutocomplete(); @@ -319,9 +327,11 @@ }; SearchAutocomplete.prototype.disableAutocomplete = function() { - this.searchInput.addClass('disabled'); - this.dropdown.removeClass('open'); - return this.restoreMenu(); + if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); + this.restoreMenu(); + } }; SearchAutocomplete.prototype.restoreMenu = function() { diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js new file mode 100644 index 00000000000..855e97eb301 --- /dev/null +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -0,0 +1,12 @@ +/*= require_tree . */ + +(function() { + $(function() { + var editor = ace.edit("editor") + + $(".snippet-form-holder form").on('submit', function() { + $(".snippet-file-content").val(editor.getValue()); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js index b46390ad8f4..6c4d88cf407 100644 --- a/app/assets/javascripts/user.js +++ b/app/assets/javascripts/user.js @@ -7,10 +7,8 @@ }); this.initTabs(); $('.hide-project-limit-message').on('click', function(e) { - var path; - path = '/'; $.cookie('hide_project_limit_message', 'false', { - path: path + path: gon.relative_url_root || '/' }); $(this).parents('.project-limit-message').remove(); return e.preventDefault(); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 8b3dbf5f5ae..74ecf4f4cf9 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -3,7 +3,6 @@ this.Calendar = (function() { function Calendar(timestamps, calendar_activities_path) { - var group, i; this.calendar_activities_path = calendar_activities_path; this.clickDay = bind(this.clickDay, this); this.currentSelectedDate = ''; @@ -13,26 +12,36 @@ this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.months = []; this.timestampsTmp = []; - i = 0; - group = 0; - _.each(timestamps, (function(_this) { - return function(count, date) { - var day, innerArray, newDate; - newDate = new Date(parseInt(date) * 1000); - day = newDate.getDay(); - if ((day === 0 && i !== 0) || i === 0) { - _this.timestampsTmp.push([]); - group++; - } - innerArray = _this.timestampsTmp[group - 1]; - innerArray.push({ - count: count, - date: newDate, - day: day - }); - return i++; - }; - })(this)); + var group = 0; + + var today = new Date() + today.setHours(0, 0, 0, 0, 0); + + var oneYearAgo = new Date(today); + oneYearAgo.setFullYear(today.getFullYear() - 1); + + var days = gl.utils.getDayDifference(oneYearAgo, today); + + for(var i = 0; i <= days; i++) { + var date = new Date(oneYearAgo); + date.setDate(date.getDate() + i); + + var day = date.getDay(); + var count = timestamps[date.getTime() * 0.001]; + + if ((day === 0 && i !== 0) || i === 0) { + this.timestampsTmp.push([]); + group++; + } + + var innerArray = this.timestampsTmp[group - 1]; + innerArray.push({ + count: count || 0, + date: date, + day: day + }); + } + this.colorKey = this.initColorKey(); this.color = this.initColor(); this.renderSvg(group); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index a306b8f3f29..d5cca1b10fb 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -24,6 +24,7 @@ @import "framework/issue_box.scss"; @import "framework/jquery.scss"; @import "framework/lists.scss"; +@import "framework/logo.scss"; @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; @import "framework/modal.scss"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 6c3786b49bb..cd3ddf5fee9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -200,7 +200,7 @@ svg { height: 15px; - width: auto; + width: 15px; position: relative; top: 2px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c1e5305644b..8984bce616c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -248,7 +248,7 @@ li.note { img.emoji { height: 20px; - vertical-align: middle; + vertical-align: top; width: 20px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index f1635a53763..edb2ff01f88 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -17,6 +17,12 @@ .dropdown { position: relative; + + .btn-link { + &:hover { + cursor: pointer; + } + } } .open { @@ -84,6 +90,15 @@ width: 100%; } } + + // Allows dynamic-width text in the dropdown toggle. + // Resizes to allow long text without overflowing the container. + &.dynamic { + width: auto; + min-width: 160px; + max-width: 100%; + padding-right: 25px; + } } .dropdown-menu, @@ -180,6 +195,12 @@ .separator + .dropdown-header { padding-top: 2px; } + + .unclickable { + cursor: not-allowed; + padding: 5px 8px; + color: $dropdown-header-color; + } } .dropdown-menu-large { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 407f1873431..d3e3fc50736 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -63,9 +63,10 @@ &.image_file { background: #eee; text-align: center; + img { - padding: 100px; - max-width: 50%; + padding: 20px; + max-width: 80%; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 43d55661541..37ff7e22ed1 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -19,7 +19,6 @@ input[type='text'].danger { } .form-actions { - margin: -$gl-padding; margin-top: 0; margin-bottom: -$gl-padding; padding: $gl-padding; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0c607071840..afe4a276ae5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -2,16 +2,6 @@ * Application Header * */ -@mixin tanuki-logo-colors($path-color) { - fill: $path-color; - transition: all 0.8s; - - &:hover, - &.highlight { - fill: lighten($path-color, 25%); - transition: all 0.1s; - } -} header { transition: padding $sidebar-transition-duration; @@ -25,7 +15,7 @@ header { margin: 8px 0; text-align: center; - #tanuki-logo, img { + .tanuki-logo, img { height: 36px; } } @@ -205,26 +195,6 @@ header { } } -#tanuki-logo { - - #tanuki-left-ear, - #tanuki-right-ear, - #tanuki-nose { - @include tanuki-logo-colors($tanuki-red); - } - - #tanuki-left-eye, - #tanuki-right-eye { - @include tanuki-logo-colors($tanuki-orange); - } - - #tanuki-left-cheek, - #tanuki-right-cheek { - @include tanuki-logo-colors($tanuki-yellow); - } - -} - @media (max-width: $screen-xs-max) { header .container-fluid { font-size: 18px; diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss new file mode 100644 index 00000000000..3ee3fb4cee5 --- /dev/null +++ b/app/assets/stylesheets/framework/logo.scss @@ -0,0 +1,118 @@ +@mixin unique-keyframes { + $animation-name: unique-id(); + @include webkit-prefix(animation-name, $animation-name); + + @-webkit-keyframes #{$animation-name} { + @content; + } + @keyframes #{$animation-name} { + @content; + } +} + +@mixin tanuki-logo-colors($path-color) { + fill: $path-color; + transition: all 0.8s; + + &:hover { + fill: lighten($path-color, 25%); + transition: all 0.1s; + } +} + +@mixin tanuki-second-highlight-animations($tanuki-color) { + @include unique-keyframes { + 10%, 80% { + fill: #{$tanuki-color} + } + 20%, 90% { + fill: lighten($tanuki-color, 25%); + } + } +} + +@mixin tanuki-forth-highlight-animations($tanuki-color) { + @include unique-keyframes { + 30%, 60% { + fill: #{$tanuki-color}; + } + 40%, 70% { + fill: lighten($tanuki-color, 25%); + } + } +} + +.tanuki-logo { + + .tanuki-left-ear, + .tanuki-right-ear, + .tanuki-nose { + @include tanuki-logo-colors($tanuki-red); + } + + .tanuki-left-eye, + .tanuki-right-eye { + @include tanuki-logo-colors($tanuki-orange); + } + + .tanuki-left-cheek, + .tanuki-right-cheek { + @include tanuki-logo-colors($tanuki-yellow); + } + + &.animate { + .tanuki-shape { + @include webkit-prefix(animation-duration, 1.5s); + @include webkit-prefix(animation-iteration-count, infinite); + } + + .tanuki-left-cheek { + @include unique-keyframes { + 0%, 10%, 100% { + fill: lighten($tanuki-yellow, 25%); + } + 90% { + fill: $tanuki-yellow; + } + } + } + + .tanuki-left-eye { + @include tanuki-second-highlight-animations($tanuki-orange); + } + + .tanuki-left-ear { + @include tanuki-second-highlight-animations($tanuki-red); + } + + .tanuki-nose { + @include unique-keyframes { + 20%, 70% { + fill: $tanuki-red; + } + 30%, 80% { + fill: lighten($tanuki-red, 25%); + } + } + } + + .tanuki-right-eye { + @include tanuki-forth-highlight-animations($tanuki-orange); + } + + .tanuki-right-ear { + @include tanuki-forth-highlight-animations($tanuki-red); + } + + .tanuki-right-cheek { + @include unique-keyframes { + 40% { + fill: $tanuki-yellow; + } + 60% { + fill: lighten($tanuki-yellow, 25%); + } + } + } + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index d2d60ed7196..396a37bab6e 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -9,22 +9,6 @@ border-radius: $radius; } -@mixin border-radius-left($radius) { - @include border-radius($radius 0 0 $radius) -} - -@mixin border-radius-right($radius) { - @include border-radius(0 0 $radius $radius) -} - -@mixin linear-gradient($from, $to) { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); - background-image: -webkit-linear-gradient($from, $to); - background-image: -moz-linear-gradient($from, $to); - background-image: -ms-linear-gradient($from, $to); - background-image: -o-linear-gradient($from, $to); -} - @mixin transition($transition) { -webkit-transition: $transition; -moz-transition: $transition; @@ -38,14 +22,6 @@ * Mixins with fixed values */ -@mixin shade { - @include box-shadow(0 0 3px #ddd); -} - -@mixin solid-shade { - @include box-shadow(0 0 0 3px #f1f1f1); -} - @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; @@ -94,23 +70,6 @@ } } -@mixin input-big { - height: 36px; - padding: 5px 10px; - font-size: 16px; - line-height: 24px; - color: #7f8fa4; - background-color: #fff; - border-color: #e7e9ed; -} - -@mixin btn-big { - height: 36px; - padding: 5px 10px; - font-size: 16px; - line-height: 24px; -} - @mixin bulleted-list { > ul { list-style-type: disc; @@ -129,3 +88,8 @@ color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.1); } + +@mixin webkit-prefix($property, $value) { + #{'-webkit-' + $property}: $value; + #{$property}: $value; +} diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 26ad2870aa0..8374f30d0b2 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,6 +1,5 @@ .modal-body { position: relative; - overflow-y: auto; padding: 15px; .form-actions { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 9e924f99e9c..9efbaf54e90 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,4 +1,4 @@ -@mixin fade($gradient-direction, $rgba, $gradient-color) { +@mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; z-index: 2; @@ -8,10 +8,7 @@ height: 30px; transition-duration: .3s; -webkit-transform: translateZ(0); - background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); - background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); &.scrolling { visibility: visible; @@ -71,7 +68,7 @@ .badge { font-weight: normal; background-color: #eee; - color: #78a; + color: $btn-transparent-color; vertical-align: baseline; } } @@ -161,6 +158,7 @@ > .dropdown { margin-right: $gl-padding-top; display: inline-block; + vertical-align: top; &:last-child { margin-right: 0; @@ -210,12 +208,6 @@ } } - .project-filter-form { - input { - background-color: $background-color; - } - } - @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; @@ -335,10 +327,6 @@ } } - .badge { - color: $gl-icon-color; - } - &:hover { a, i { color: $black; @@ -356,7 +344,7 @@ } .fade-right { - @include fade(left, rgba(255, 255, 255, 0.4), $background-color); + @include fade(left, $background-color); right: -5px; .fa { @@ -365,7 +353,7 @@ } .fade-left { - @include fade(right, rgba(255, 255, 255, 0.4), $background-color); + @include fade(right, $background-color); left: -5px; .fa { @@ -376,6 +364,7 @@ &.sub-nav-scroll { .fade-right { + @include fade(left, $dark-background-color); right: 0; .fa { @@ -384,6 +373,7 @@ } .fade-left { + @include fade(right, $dark-background-color); left: 0; .fa { @@ -400,7 +390,7 @@ @include scrolling-links(); .fade-right { - @include fade(left, rgba(255, 255, 255, 0.4), $white-light); + @include fade(left, $white-light); right: -5px; .fa { @@ -409,7 +399,7 @@ } .fade-left { - @include fade(right, rgba(255, 255, 255, 0.4), $white-light); + @include fade(right, $white-light); left: -5px; .fa { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 21d87cc9d34..b2e22b60440 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -45,7 +45,8 @@ min-width: 175px; } -.select2-results .select2-result-label { +.select2-results .select2-result-label, +.select2-more-results { padding: 10px 15px; } diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index c9cdfdcd29c..8f71381f5c4 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -96,6 +96,10 @@ line-height: inherit; } } + + .label-default { + color: $btn-transparent-color; + } } .abuse-reports { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ad4b2d6496f..d91558bc672 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -8,9 +8,13 @@ } .is-dragging { + // Important because plugin sets inline CSS + opacity: 1!important; + * { - cursor: -webkit-grabbing; - cursor: grabbing; + // !important to make sure no style can override this when dragging + cursor: -webkit-grabbing!important; + cursor: grabbing!important; } } @@ -101,8 +105,8 @@ .board { display: -webkit-flex; display: flex; - min-width: calc(100vw - 15px); - max-width: calc(100vw - 15px); + min-width: calc(85vw - 15px); + max-width: calc(85vw - 15px); margin-bottom: 25px; padding-right: ($gl-padding / 2); padding-left: ($gl-padding / 2); @@ -154,48 +158,6 @@ padding: $gl-padding; font-size: 1em; border-bottom: 1px solid $border-color; - - .board-mobile-handle { - position: relative; - left: 0; - top: 1px; - margin-top: 0; - margin-right: 5px; - } -} - -.board-search-container { - position: relative; - background-color: #fff; - - .form-control { - padding-right: 30px; - } -} - -.board-search-icon, -.board-search-clear-btn { - position: absolute; - right: $gl-padding + 10px; - top: 50%; - margin-top: -7px; - font-size: 14px; -} - -.board-search-icon { - color: $gl-placeholder-color; -} - -.board-search-clear-btn { - padding: 0; - line-height: 1; - background: transparent; - border: 0; - outline: 0; - - &:hover { - color: $gl-link-color; - } } .board-delete { @@ -254,11 +216,6 @@ opacity: 0.3; } -.is-dragging { - // Important because plugin sets inline CSS - opacity: 1!important; -} - .card { position: relative; width: 100%; @@ -269,11 +226,7 @@ list-style: none; &.user-can-drag { - padding-left: ($gl-padding * 2); - - @media (min-width: $screen-sm-min) { - padding-left: $gl-padding; - } + padding-left: $gl-padding; } &:not(:last-child) { @@ -294,17 +247,6 @@ } } -.board-mobile-handle { - position: absolute; - left: 10px; - top: 50%; - margin-top: (-15px / 2); - - @media (min-width: $screen-sm-min) { - display: none; - } -} - .card-title { margin: 0; font-size: 1em; @@ -316,6 +258,7 @@ .card-footer { margin-top: 5px; + line-height: 25px; .label { margin-right: 4px; @@ -327,3 +270,12 @@ margin-right: 8px; font-weight: 500; } + +.issue-boards-search { + width: 335px; + + .form-control { + display: inline-block; + width: 210px; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 81fce55853c..8c33e7d9a2e 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -150,7 +150,7 @@ border-top: 1px solid $border-color; border-bottom: 1px solid $border-color; max-height: 300px; - overflow: scroll; + overflow: auto; svg { position: relative; @@ -168,7 +168,6 @@ text-overflow: ellipsis; &:hover { - background-color: $row-hover; color: $gl-text-color; } } @@ -190,6 +189,10 @@ display: block; } } + + &:hover { + background-color: $row-hover; + } } } } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index fe8a8025038..53ec0002afe 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -68,22 +68,11 @@ } .ci-status-link { - margin-left: 2px; svg { - vertical-align: middle; - } - - .ci-status-label { - display: inline-block; - } - - &:hover { - text-decoration: none; - - .ci-status-label { - text-decoration: underline; - } + position: relative; + top: 2px; + margin: 0 2px 0 3px; } } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5c336bb1c7e..0cd45fb90bf 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -115,11 +115,8 @@ } &.commits-stat { - margin-top: 3px; display: block; - padding: 3px; - padding-left: 0; - + padding: 0 3px 0 0; &:hover { background: none; } diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index 84cc35239f9..a4f76a9495a 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -1,22 +1,3 @@ -i.icon-gitorious { - display: inline-block; - background-position: 0 0; - background-size: contain; - background-repeat: no-repeat; -} - -i.icon-gitorious-small { - background-image: image-url('gitorious-logo-blue.png'); - width: 13px; - height: 13px; -} - -i.icon-gitorious-big { - background-image: image-url('gitorious-logo-black.png'); - width: 18px; - height: 18px; -} - .import-jobs-from-col, .import-jobs-to-col { width: 40%; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b4636269518..7fdd79fa8b9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -269,7 +269,7 @@ .builds { .table-holder { - overflow-x: scroll; + overflow-x: auto; } } @@ -374,3 +374,20 @@ } } } + +.mr-version-switch { + background: $background-color; + padding: $gl-btn-padding; + color: $gl-placeholder-color; + + a.btn-link { + color: $gl-dark-link-color; + } +} + +.merge-request-details { + + .title { + margin-bottom: 20px; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 08d1692c888..54124a3d658 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -281,19 +281,13 @@ ul.notes { font-size: 17px; } - &.js-note-delete { - i { - &:hover { - color: $gl-text-red; - } + &:hover { + .danger-highlight { + color: $gl-text-red; } - } - &.js-note-edit { - i { - &:hover { - color: $gl-link-color; - } + .link-highlight { + color: $gl-link-color; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ce1c424624f..2d6653cd867 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -254,7 +254,6 @@ width: 100%; overflow: auto; white-space: nowrap; - max-height: 500px; transition: max-height 0.3s, padding 0.3s; &.graph-collapsed { @@ -265,7 +264,6 @@ .pipeline-visualization { position: relative; - min-width: 1220px; ul { padding: 0; @@ -275,7 +273,7 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 50px; + margin-right: 65px; li { list-style: none; @@ -300,6 +298,17 @@ &.playable { background-color: $gray-light; + + svg { + height: 12px; + width: 12px; + position: relative; + top: 1px; + + path { + fill: $layout-link-gray; + } + } } .build-content { @@ -310,6 +319,14 @@ a { color: $layout-link-gray; + text-decoration: none; + + &:hover { + .ci-status-text { + text-decoration: underline; + } + } + } } @@ -319,19 +336,15 @@ margin-right: 5px; } - .fa { - font-size: 13px; - } - // Connect first build in each stage with right horizontal line &:first-child { &::after { content: ''; position: absolute; top: 50%; - right: -54px; + right: -69px; border-top: 2px solid $border-color; - width: 54px; + width: 69px; height: 1px; } } @@ -351,22 +364,25 @@ &::after { right: -20px; border-right: 2px solid $border-color; - border-radius: 0 0 50px; + border-radius: 0 0 15px; } // Left connecting curves &::before { left: -20px; border-left: 2px solid $border-color; - border-radius: 0 0 0 50px; + border-radius: 0 0 0 15px; } } // Connect second build to first build with smaller curved line &:nth-child(2) { &::after, &::before { - height: 45px; - top: -26px; + height: 29px; + top: -10px; + } + .curve { + display: block; } } } @@ -385,6 +401,12 @@ border: none; } } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } } } @@ -396,6 +418,39 @@ border: none; } } + // Remove opposite curve + .curve { + &::after { + display: none; + } + } + } + } + + // Curve first child connecting lines in opposite direction + .curve { + display: none; + + &::before, + &::after { + content: ''; + width: 21px; + height: 25px; + position: absolute; + top: -28.5px; + border-top: 2px solid $border-color; + } + + &::after { + left: -39px; + border-right: 2px solid $border-color; + border-radius: 0 15px; + } + + &::before { + right: -39px; + border-left: 2px solid $border-color; + border-radius: 15px 0 0; } } } @@ -422,3 +477,10 @@ width: 60px; } } + +.ci-status-icon-created { + + svg { + fill: $table-text-gray; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 27dc2b2a1fa..f2db373da52 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -311,6 +311,14 @@ a.deploy-project-label { color: $gl-success; } +.lfs-enabled { + color: $gl-success; +} + +.lfs-disabled { + color: $gl-warning; +} + .breadcrumb.repo-breadcrumb { padding: 0; background: transparent; @@ -600,18 +608,25 @@ pre.light-well { } } -.project-show-readme .readme-holder { - padding: $gl-padding 0; - border-top: 0; - - .edit-project-readme { - z-index: 2; - position: relative; +.project-show-readme { + .row-content-block { + background-color: inherit; + border: none; } - .wiki h1 { - border-bottom: none; - padding: 0; + .readme-holder { + padding: $gl-padding 0; + border-top: 0; + + .edit-project-readme { + z-index: 2; + position: relative; + } + + .wiki h1 { + border-bottom: none; + padding: 0; + } } } @@ -719,3 +734,29 @@ pre.light-well { width: 300px; } } + +.clearable-input { + position: relative; + + .clear-icon { + @extend .fa-times; + display: none; + position: absolute; + right: 7px; + top: 7px; + color: $location-icon-color; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + &.has-value { + .clear-icon { + cursor: pointer; + display: block; + } + } +} diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 587f2d9f3c1..0ee7ceecae5 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -43,6 +43,15 @@ border-color: $blue-normal; } + &.ci-created { + color: $table-text-gray; + border-color: $table-text-gray; + + svg { + fill: $table-text-gray; + } + } + svg { height: 13px; width: 13px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9da40fe2b09..538f211c65b 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -11,6 +11,16 @@ } } + .last-commit { + max-width: 506px; + + .last-commit-content { + @include str-truncated; + width: calc(100% - 140px); + margin-left: 3px; + } + } + .tree-table { margin-bottom: 0; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9e1dc15de84..6ef7cf0bae6 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -109,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :sentry_dsn, :akismet_enabled, :akismet_api_key, + :koding_enabled, + :koding_url, :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4ce18321649..cdfa8d91a28 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController end def members_update - @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user) redirect_to [:admin, @group], notice: 'Users were successfully added.' end diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb index 8be35f00a77..9433da02f64 100644 --- a/app/controllers/admin/impersonations_controller.rb +++ b/app/controllers/admin/impersonations_controller.rb @@ -7,7 +7,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController warden.set_user(impersonator, scope: :user) - Gitlab::AppLogger.info("User #{original_user.username} has stopped impersonating #{impersonator.username}") + Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}") session[:impersonator_id] = nil diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index e4c73008826..ca04a17caa1 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -29,7 +29,8 @@ class Admin::SystemInfoController < Admin::ApplicationController ] def show - system_info = Vmstat.snapshot + @cpus = Vmstat.cpu rescue nil + @memory = Vmstat.memory rescue nil mounts = Sys::Filesystem.mounts @disks = [] @@ -50,10 +51,5 @@ class Admin::SystemInfoController < Admin::ApplicationController rescue Sys::Filesystem::Error end end - - @cpus = system_info.cpus.length - - @mem_used = system_info.memory.active_bytes - @mem_total = system_info.memory.total_bytes end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 634d36a4467..ebc2a4651ba 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper + include SentryHelper include WorkhorseHelper before_action :authenticate_user_from_private_token! @@ -24,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -46,28 +47,6 @@ class ApplicationController < ActionController::Base protected - def sentry_context - if Rails.env.production? && current_application_settings.sentry_enabled - if current_user - Raven.user_context( - id: current_user.id, - email: current_user.email, - username: current_user.username, - ) - end - - Raven.tags_context(program: sentry_program_context) - end - end - - def sentry_program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end - end - # This filter handles both private tokens and personal access tokens def authenticate_user_from_private_token! token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence @@ -271,10 +250,6 @@ class ApplicationController < ActionController::Base Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? end - def gitorious_import_enabled? - current_application_settings.import_sources.include?('gitorious') - end - def google_code_import_enabled? current_application_settings.import_sources.include?('google_code') end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 036777c80c1..172d5344b7a 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -8,10 +8,14 @@ module ToggleAwardEmoji def toggle_award_emoji name = params.require(:name) - awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + if awardable.user_can_award?(current_user, name) + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(to_todoable(awardable), current_user) - render json: { ok: true } + render json: { ok: true } + else + render json: { ok: false } + end end private diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index c8390af3b36..d425d0f9014 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController before_action :find_todos, only: [:index, :destroy_all] def index + @sort = params[:sort] @todos = @todos.page(params[:page]) end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 9fc41a12536..272164cd0cc 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController end def create - @group.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @group.add_users( + params[:user_ids].split(','), + params[:access_level], + current_user: current_user, + expires_at: params[:expires_at] + ) redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' end @@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController protected def member_params - params.require(:group_member).permit(:access_level, :user_id) + params.require(:group_member).permit(:access_level, :user_id, :expires_at) end # MembershipActions concern diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb deleted file mode 100644 index a4c4ad23027..00000000000 --- a/app/controllers/import/gitorious_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -class Import::GitoriousController < Import::BaseController - before_action :verify_gitorious_import_enabled - - def new - redirect_to client.authorize_url(callback_import_gitorious_url) - end - - def callback - session[:gitorious_repos] = params[:repos] - redirect_to status_import_gitorious_path - end - - def status - @repos = client.repos - - @already_added_projects = current_user.created_projects.where(import_type: "gitorious") - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } - end - - def jobs - jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status]) - render json: jobs - end - - def create - @repo_id = params[:repo_id] - repo = client.repo(@repo_id) - @target_namespace = params[:new_namespace].presence || repo.namespace - @project_name = repo.name - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute - end - - private - - def client - @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos]) - end - - def verify_gitorious_import_enabled - render_404 unless gitorious_import_enabled? - end -end diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb new file mode 100644 index 00000000000..f3759b4c0ea --- /dev/null +++ b/app/controllers/koding_controller.rb @@ -0,0 +1,15 @@ +class KodingController < ApplicationController + before_action :check_integration!, :authenticate_user!, :reject_blocked! + layout 'koding' + + def index + path = File.join(Rails.root, 'doc/user/project/koding.md') + @markdown = File.read(path) + end + + private + + def check_integration! + render_404 unless current_application_settings.koding_enabled? + end +end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 7241949393b..59222637961 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,22 +1,25 @@ class Projects::ArtifactsController < Projects::ApplicationController + include ExtractsPath + layout 'project' before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] + before_action :extract_ref_name_and_path before_action :validate_artifacts! def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url + if artifacts_file.file_storage? + send_file artifacts_file.path, disposition: 'attachment' + else + redirect_to artifacts_file.url end - - send_file artifacts_file.path, disposition: 'attachment' end def browse directory = params[:path] ? "#{params[:path]}/" : '' @entry = build.artifacts_metadata_entry(directory) - return render_404 unless @entry.exists? + render_404 unless @entry.exists? end def file @@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController redirect_to namespace_project_build_path(project.namespace, project, build) end + def latest_succeeded + target_path = artifacts_action_path(@path, project, build) + + if target_path + redirect_to(target_path) + else + render_404 + end + end + private + def extract_ref_name_and_path + return unless params[:ref_name_and_path] + + @ref_name, @path = extract_ref(params[:ref_name_and_path]) + end + def validate_artifacts! - render_404 unless build.artifacts? + render_404 unless build && build.artifacts? end def build - @build ||= project.builds.find_by!(id: params[:build_id]) + @build ||= build_from_id || build_from_ref + end + + def build_from_id + project.builds.find_by(id: params[:build_id]) if params[:build_id] + end + + def build_from_ref + return unless @ref_name + + builds = project.latest_successful_builds_for(@ref_name) + builds.find_by(name: params[:job]) end def artifacts_file diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 5962f74c39b..ada7db3c552 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -4,7 +4,7 @@ class Projects::AvatarsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def show - @blob = @repository.blob_at_branch('master', @project.avatar_in_git) + @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 2d894b3dd4a..1a4f6b50e8f 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -12,7 +12,7 @@ module Projects only: [:iid, :title, :confidential], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority] } + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } }) end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 48fe81b0d74..2de8ada3e29 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController diverging_commit_counts = repository.diverging_commit_counts(branch) [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max end + + respond_to do |format| + format.html + format.json do + render json: @repository.branch_names + end + end end def recent diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 606552fa853..d0c4550733c 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController return render_404 unless can?(current_user, :read_group, group) project.project_group_links.create( - group: group, group_access: params[:link_group_access] + group: group, + group_access: params[:link_group_access], + expires_at: params[:expires_at] ) redirect_to namespace_project_group_links_path(project.namespace, project) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 639cf4c0ef2..7c03dcd2e64 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -125,6 +125,10 @@ class Projects::IssuesController < Projects::ApplicationController render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) end end + + rescue ActiveRecord::StaleObjectError + @conflict = true + render :edit end def referenced_merge_requests @@ -208,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController if action_name == 'new' redirect_to external.new_issue_path else - redirect_to external.issues_url + redirect_to external.project_path end end @@ -230,7 +234,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :due_date, :state_event, :task_num, label_ids: [] + :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d3fe441c4d2..4f5f3b6aa09 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -83,12 +83,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @merge_request_diff = @merge_request.merge_request_diff + @merge_request_diff = + if params[:diff_id] + @merge_request.merge_request_diffs.find(params[:diff_id]) + else + @merge_request.merge_request_diff + end respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diffs(diff_options) + unless @merge_request_diff.latest? + # Disable comments if browsing older version of the diff + @diff_notes_disabled = true + end + + @diffs = @merge_request_diff.diffs(diff_options) render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -258,6 +268,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController else render "edit" end + rescue ActiveRecord::StaleObjectError + @conflict = true + render :edit end def remove_wip @@ -493,7 +506,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :state_event, :description, :task_num, :force_remove_source_branch, - label_ids: [] + :lock_version, label_ids: [] ) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3435a118964..42a7e5a2c30 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @project.team.add_users( + params[:user_ids].split(','), + params[:access_level], + expires_at: params[:expires_at], + current_user: current_user + ) redirect_to namespace_project_project_members_path(@project.namespace, @project) end @@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController protected def member_params - params.require(:project_member).permit(:user_id, :access_level) + params.require(:project_member).permit(:user_id, :access_level, :expires_at) end # MembershipActions concern diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fc52cd2f367..84d6b106cd7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -309,7 +309,7 @@ class ProjectsController < Projects::ApplicationController :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled + :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :lfs_enabled ) end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb index 3334b8556df..79eb45568be 100644 --- a/app/finders/move_to_project_finder.rb +++ b/app/finders/move_to_project_finder.rb @@ -1,4 +1,6 @@ class MoveToProjectFinder + PAGE_SIZE = 50 + def initialize(user) @user = user end @@ -8,6 +10,10 @@ class MoveToProjectFinder projects = projects.search(search) if search.present? projects = projects.excluding_project(from_project) + # infinite scroll using offset + projects = projects.where('projects.id < ?', offset_id) if offset_id.present? + projects = projects.limit(PAGE_SIZE) + # to ask for Project#name_with_namespace projects.includes(namespace: :owner) end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 37bad596a16..06b3e8a9502 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -33,7 +33,7 @@ class TodosFinder # the project IDs yielded by the todos query thus far items = by_project(items) - items.reorder(id: :desc) + sort(items) end private @@ -106,6 +106,10 @@ class TodosFinder params[:type] end + def sort(items) + params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) + end + def by_action(items) if action? items = items.where(action: to_action_id) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 78c0b79d2bd..6de25bea654 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -31,6 +31,10 @@ module ApplicationSettingsHelper current_application_settings.akismet_enabled? end + def koding_enabled? + current_application_settings.koding_enabled? + end + def allowed_protocols_present? current_application_settings.enabled_git_access_protocol.present? end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9ea03720c1e..e13b7cdd707 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -217,4 +217,12 @@ module BlobHelper def gitlab_ci_ymls @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names end + + def blob_editor_paths + { + 'relative-url-root' => Rails.application.config.relative_url_root, + 'assets-prefix' => Gitlab::Application.config.assets.prefix, + 'blob-language' => @blob && @blob.language.try(:ace_mode) + } + end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 94df7d131ca..639deb7c521 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -25,6 +25,11 @@ module CiStatusHelper end end + def ci_status_for_statuseable(subject) + status = subject.try(:status) || 'not found' + status.humanize + end + def ci_icon_for_status(status) icon_name = case status @@ -39,9 +44,9 @@ module CiStatusHelper when 'running' 'icon_status_running' when 'play' - return icon('play fw') + 'icon_play' when 'created' - 'icon_status_pending' + 'icon_status_created' else 'icon_status_cancel' end @@ -66,10 +71,10 @@ module CiStatusHelper Ci::Runner.shared.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '') + def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body') klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" - data = { toggle: 'tooltip', placement: tooltip_placement } + data = { toggle: 'tooltip', placement: tooltip_placement, container: container } if path link_to ci_icon_for_status(status), path, diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5386ddadc62..a322a90cc4e 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -149,4 +149,20 @@ module GitlabRoutingHelper def resend_invite_group_member_path(group_member, *args) resend_invite_group_group_member_path(group_member.source, group_member) end + + # Artifacts + + def artifacts_action_path(path, project, build) + action, path_params = path.split('/', 2) + args = [project.namespace, project, build, path_params] + + case action + when 'download' + download_namespace_project_build_artifacts_path(*args) + when 'browse' + browse_namespace_project_build_artifacts_path(*args) + when 'file' + file_namespace_project_build_artifacts_path(*args) + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 47d174361db..b9baeb1d6c4 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -72,6 +72,15 @@ module IssuablesHelper end end + def issuable_labels_tooltip(labels, limit: 5) + first, last = labels.partition.with_index{ |_, i| i < limit } + + label_names = first.collect(&:name) + label_names << "and #{last.size} more" unless last.empty? + + label_names.join(', ') + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index eb651e3687e..5d82abfca79 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -23,10 +23,14 @@ module LfsHelper end def lfs_download_access? + return false unless project.lfs_enabled? + project.public? || ci? || (user && user.can?(:download_code, project)) end def lfs_upload_access? + return false unless project.lfs_enabled? + user && user.can?(:push_code, project) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index db6e731c744..a9e175c3f5c 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -98,6 +98,6 @@ module MergeRequestsHelper end def merge_request_button_visibility(merge_request, closed) - return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6c1cc6ef072..2b0ff6c0d00 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -25,6 +25,8 @@ module NavHelper current_path?('merge_requests#commits') || current_path?('merge_requests#builds') || current_path?('merge_requests#conflicts') || + current_path?('merge_requests#pipelines') || + current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 505545fbabb..f07077bd133 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -116,6 +116,17 @@ module ProjectsHelper license.nickname || license.name end + def last_push_event + return unless current_user + + project_ids = [@project.id] + if fork = current_user.fork_of(@project) + project_ids << fork.id + end + + current_user.recent_push(project_ids) + end + private def get_project_nav_tabs(project, current_user) @@ -176,6 +187,18 @@ module ProjectsHelper nav_tabs.flatten end + def project_lfs_status(project) + if project.lfs_enabled? + content_tag(:span, class: 'lfs-enabled') do + 'Enabled' + end + else + content_tag(:span, class: 'lfs-disabled') do + 'Disabled' + end + end + end + def git_user_name if current_user current_user.name @@ -236,6 +259,60 @@ module ProjectsHelper ) end + def add_koding_stack_path(project) + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch || 'master', + file_name: '.koding.yml', + commit_message: "Add Koding stack script", + content: <<-CONTENT.strip_heredoc + provider: + aws: + access_key: '${var.aws_access_key}' + secret_key: '${var.aws_secret_key}' + resource: + aws_instance: + #{project.path}-vm: + instance_type: t2.nano + user_data: |- + + # Created by GitLab UI for :> + + echo _KD_NOTIFY_@Installing Base packages...@ + + apt-get update -y + apt-get install git -y + + echo _KD_NOTIFY_@Cloning #{project.name}...@ + + export KODING_USER=${var.koding_user_username} + export REPO_URL=#{root_url}${var.koding_queryString_repo}.git + export BRANCH=${var.koding_queryString_branch} + + sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH + + echo _KD_NOTIFY_@#{project.name} cloned.@ + CONTENT + ) + end + + def koding_project_url(project = nil, branch = nil, sha = nil) + if project + import_path = "/Home/Stacks/import" + + repo = project.path_with_namespace + branch ||= project.default_branch + sha ||= project.commit.short_id + + path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}" + + return URI.join(current_application_settings.koding_url, path).to_s + end + + current_application_settings.koding_url + end + def contribution_guide_path(project) if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( @@ -297,16 +374,6 @@ module ProjectsHelper namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') end - def last_push_event - return unless current_user - - if fork = current_user.fork_of(@project) - current_user.recent_push(fork.id) - else - current_user.recent_push(@project.id) - end - end - def readme_cache_key sha = @project.commit.try(:sha) || 'nil' [@project.path_with_namespace, sha, "readme"].join('-') diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c0195713f4a..4549c2e5bb6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -44,7 +44,7 @@ module SearchHelper def help_autocomplete [ { category: "Help", label: "API Help", url: help_page_path("api/README") }, - { category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") }, + { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") }, { category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") }, { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") }, { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb new file mode 100644 index 00000000000..f8cccade15b --- /dev/null +++ b/app/helpers/sentry_helper.rb @@ -0,0 +1,27 @@ +module SentryHelper + def sentry_enabled? + Rails.env.production? && current_application_settings.sentry_enabled? + end + + def sentry_context + return unless sentry_enabled? + + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end + + Raven.tags_context(program: sentry_program_context) + end + + def sentry_program_context + if Sidekiq.server? + 'sidekiq' + else + 'rails' + end + end +end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 790001222f1..271e839692a 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -15,20 +15,9 @@ module TimeHelper "#{from.to_s(:short)} - #{to.to_s(:short)}" end - def duration_in_numbers(finished_at, started_at) - interval = interval_in_seconds(started_at, finished_at) - time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S" + def duration_in_numbers(duration) + time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S" - Time.at(interval).utc.strftime(time_format) - end - - private - - def interval_in_seconds(started_at, finished_at = nil) - if started_at && finished_at - finished_at.to_i - started_at.to_i - elsif started_at - Time.now.to_i - started_at.to_i - end + Time.at(duration).utc.strftime(time_format) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 07f703f205d..c1df4a865f6 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -166,38 +166,44 @@ class Ability end def project_abilities(user, project) - rules = [] key = "/user/#{user.id}/project/#{project.id}" - RequestStore.store[key] ||= begin - # Push abilities on the users team role - rules.push(*project_team_rules(project.team, user)) + if RequestStore.active? + RequestStore.store[key] ||= uncached_project_abilities(user, project) + else + uncached_project_abilities(user, project) + end + end - owner = user.admin? || - project.owner == user || - (project.group && project.group.has_owner?(user)) + def uncached_project_abilities(user, project) + rules = [] + # Push abilities on the users team role + rules.push(*project_team_rules(project.team, user)) - if owner - rules.push(*project_owner_rules) - end + owner = user.admin? || + project.owner == user || + (project.group && project.group.has_owner?(user)) - if project.public? || (project.internal? && !user.external?) - rules.push(*public_project_rules) + if owner + rules.push(*project_owner_rules) + end - # Allow to read builds for internal projects - rules << :read_build if project.public_builds? + if project.public? || (project.internal? && !user.external?) + rules.push(*public_project_rules) - unless owner || project.team.member?(user) || project_group_member?(project, user) - rules << :request_access if project.request_access_enabled - end - end + # Allow to read builds for internal projects + rules << :read_build if project.public_builds? - if project.archived? - rules -= project_archived_rules + unless owner || project.team.member?(user) || project_group_member?(project, user) + rules << :request_access if project.request_access_enabled end + end - rules - project_disabled_features_rules(project) + if project.archived? + rules -= project_archived_rules end + + (rules - project_disabled_features_rules(project)).uniq end def project_team_rules(team, user) @@ -349,7 +355,7 @@ class Ability rules += named_abilities('project_snippet') end - unless project.wiki_enabled + unless project.has_wiki? rules += named_abilities('wiki') end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 8c19d9dc9c8..246477ffe88 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :akismet_enabled + validates :koding_url, + presence: true, + if: :koding_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -142,13 +146,15 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], domain_whitelist: Settings.gitlab['domain_whitelist'], - import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project], + import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, two_factor_grace_period: 48, recaptcha_enabled: false, akismet_enabled: false, + koding_enabled: false, + koding_url: nil, repository_checks_enabled: true, disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ed056a07a49..23c8de6f650 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -62,6 +62,7 @@ module Ci status_event: 'enqueue' ) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) + build.pipeline.mark_as_processable_after_stage(build.stage_idx) new_build end end @@ -351,7 +352,7 @@ module Ci end def artifacts? - !artifacts_expired? && artifacts_file.exists? + !artifacts_expired? && self[:artifacts_file].present? end def artifacts_metadata? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c360a6ff729..bd1737c7587 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1,7 +1,7 @@ module Ci class Pipeline < ActiveRecord::Base extend Ci::Model - include Statuseable + include HasStatus self.table_name = 'ci_commits' @@ -65,8 +65,8 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest_successful_for, ->(ref = default_branch) do - where(ref: ref).success.order(id: :desc).limit(1) + def self.latest_successful_for(ref) + where(ref: ref).order(id: :desc).success.first end def self.truncate_sha(sha) @@ -78,8 +78,12 @@ module Ci CommitStatus.where(pipeline: pluck(:id)).stages end + def self.total_duration + where.not(duration: nil).sum(:duration) + end + def stages_with_latest_statuses - statuses.latest.order(:stage_idx).group_by(&:stage) + statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) end def project_id @@ -146,6 +150,10 @@ module Ci end end + def mark_as_processable_after_stage(stage_idx) + builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) + end + def latest? return false unless ref commit = project.commit(ref) @@ -250,7 +258,7 @@ module Ci end def update_duration - self.duration = statuses.latest.duration + self.duration = calculate_duration end def execute_hooks diff --git a/app/models/commit.rb b/app/models/commit.rb index cc413448ce8..817d063e4a2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -229,7 +229,7 @@ class Commit def diff_refs Gitlab::Diff::DiffRefs.new( - base_sha: self.parent_id || self.sha, + base_sha: self.parent_id || Gitlab::Git::BLANK_SHA, head_sha: self.sha ) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 703ca90edb6..4a628924499 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,5 +1,5 @@ class CommitStatus < ActiveRecord::Base - include Statuseable + include HasStatus include Importable self.table_name = 'ci_builds' @@ -21,15 +21,22 @@ class CommitStatus < ActiveRecord::Base where(id: max_id.group(:name, :commit_id)) end + scope :retried, -> { where.not(id: latest) } scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } + scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } + scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } state_machine :status do event :enqueue do transition [:created, :skipped] => :pending end + event :process do + transition skipped: :created + end + event :run do transition pending: :running end @@ -107,13 +114,7 @@ class CommitStatus < ActiveRecord::Base end def duration - duration = - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end - duration + calculate_duration end def stuck? diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 800a16ab246..83f5bc1fa9e 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -59,6 +59,18 @@ module Awardable true end + def awardable_votes?(name) + AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name + end + + def user_can_award?(current_user, name) + if user_authored?(current_user) + !awardable_votes?(normalize_name(name)) + else + true + end + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb new file mode 100644 index 00000000000..be93435453b --- /dev/null +++ b/app/models/concerns/expirable.rb @@ -0,0 +1,15 @@ +module Expirable + extend ActiveSupport::Concern + + included do + scope :expired, -> { where('expires_at <= ?', Time.current) } + end + + def expires? + expires_at.present? + end + + def expires_soon? + expires_at < 7.days.from_now + end +end diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/has_status.rb index 5d4b0a86899..f7b8352405c 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/has_status.rb @@ -1,4 +1,4 @@ -module Statuseable +module HasStatus extend ActiveSupport::Concern AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] @@ -35,11 +35,6 @@ module Statuseable all.pluck(self.status_sql).first end - def duration - duration_array = all.map(&:duration).compact - duration_array.reduce(:+) - end - def started_at all.minimum(:started_at) end @@ -85,4 +80,14 @@ module Statuseable def complete? COMPLETED_STATUSES.include?(status) end + + private + + def calculate_duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index cbae1cd439b..22231b2e0f0 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -87,6 +87,12 @@ module Issuable User.find(assignee_id_was).update_cache_counts if assignee_id_was assignee.update_cache_counts if assignee end + + # We want to use optimistic lock for cases when only title or description are involved + # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html + def locking_enabled? + title_changed? || description_changed? + end end module ClassMethods @@ -131,7 +137,10 @@ module Issuable end def order_labels_priority(excluded_labels: []) - select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority"). + condition_field = "#{table_name}.id" + highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql + + select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). group(arel_table[:id]). reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end @@ -159,20 +168,6 @@ module Issuable grouping_columns end - - private - - def highest_label_priority(excluded_labels) - query = Label.select(Label.arel_table[:priority].minimum). - joins(:label_links). - where(label_links: { target_type: name }). - where("label_links.target_id = #{table_name}.id"). - reorder(nil) - - query.where.not(title: excluded_labels) if excluded_labels.present? - - query - end end def today? @@ -201,6 +196,10 @@ module Issuable end end + def user_authored?(user) + user == author + end + def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index 4be6a2f621b..b8dd27a7afe 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -17,6 +17,10 @@ module NoteOnDiff raise NotImplementedError end + def original_line_code + raise NotImplementedError + end + def diff_attributes raise NotImplementedError end @@ -24,4 +28,8 @@ module NoteOnDiff def can_be_award_emoji? false end + + def to_discussion + Discussion.new([self]) + end end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 8b47b9e0abd..1ebecd86af9 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -35,5 +35,19 @@ module Sortable all end end + + private + + def highest_label_priority(object_types, condition_field, excluded_labels: []) + query = Label.select(Label.arel_table[:priority].minimum). + joins(:label_links). + where(label_links: { target_type: object_types }). + where("label_links.target_id = #{condition_field}"). + reorder(nil) + + query.where.not(title: excluded_labels) if excluded_labels.present? + + query + end end end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index ce54fe5d3bf..1aa97debe42 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -23,7 +23,7 @@ module Spammable def submittable_as_spam? if user_agent_detail - user_agent_detail.submittable? + user_agent_detail.submittable? && current_application_settings.akismet_enabled else false end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index df2a9e3e84b..a3ac577cf3e 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -52,11 +52,11 @@ module Taskable end # Return a string that describes the current state of this Taskable's task - # list items, e.g. "20 tasks (12 completed, 8 remaining)" + # list items, e.g. "12 of 20 tasks completed" def task_status return '' if description.blank? sum = tasks.summary - "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)" + "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed" end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index f56c3d74ae3..4442cefc7e9 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -16,6 +16,9 @@ class DiffNote < Note after_initialize :ensure_original_discussion_id before_validation :set_original_position, :update_position, on: :create before_validation :set_line_code, :set_original_discussion_id + # We need to do this again, because it's already in `Note`, but is affected by + # `update_position` and needs to run after that. + before_validation :set_discussion_id after_save :keep_around_commits class << self @@ -57,6 +60,10 @@ class DiffNote < Note diff_file.position(line) == self.original_position end + def original_line_code + self.diff_file.line_code(self.diff_line) + end + def active?(diff_refs = nil) return false unless supported? return true if for_commit? @@ -100,10 +107,6 @@ class DiffNote < Note self.noteable.find_diff_discussion(self.discussion_id) end - def to_discussion - Discussion.new([self]) - end - private def supported? diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 3fddc084af2..9676bc03470 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -12,6 +12,7 @@ class Discussion :for_merge_request?, :line_code, + :original_line_code, :diff_file, :for_line?, :active?, diff --git a/app/models/group.rb b/app/models/group.rb index 37631b99701..c48869ae465 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -95,34 +95,40 @@ class Group < Namespace end end - def add_users(user_ids, access_level, current_user = nil) + def add_users(user_ids, access_level, current_user: nil, expires_at: nil) user_ids.each do |user_id| - Member.add_user(self.group_members, user_id, access_level, current_user) + Member.add_user( + self.group_members, + user_id, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end - def add_user(user, access_level, current_user = nil) - add_users([user], access_level, current_user) + def add_user(user, access_level, current_user: nil, expires_at: nil) + add_users([user], access_level, current_user: current_user, expires_at: expires_at) end def add_guest(user, current_user = nil) - add_user(user, Gitlab::Access::GUEST, current_user) + add_user(user, Gitlab::Access::GUEST, current_user: current_user) end def add_reporter(user, current_user = nil) - add_user(user, Gitlab::Access::REPORTER, current_user) + add_user(user, Gitlab::Access::REPORTER, current_user: current_user) end def add_developer(user, current_user = nil) - add_user(user, Gitlab::Access::DEVELOPER, current_user) + add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user) end def add_master(user, current_user = nil) - add_user(user, Gitlab::Access::MASTER, current_user) + add_user(user, Gitlab::Access::MASTER, current_user: current_user) end def add_owner(user, current_user = nil) - add_user(user, Gitlab::Access::OWNER, current_user) + add_user(user, Gitlab::Access::OWNER, current_user: current_user) end def has_owner?(user) diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 8e26cbe9835..40277a9b139 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -49,6 +49,10 @@ class LegacyDiffNote < Note !line.meta? && diff_file.line_code(line) == self.line_code end + def original_line_code + self.line_code + end + # Check if this note is part of an "active" discussion # # This will always return true for anything except MergeRequest noteables, diff --git a/app/models/member.rb b/app/models/member.rb index 24ab1276ee9..64e0d33fb20 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,6 +1,7 @@ class Member < ActiveRecord::Base include Sortable include Importable + include Expirable include Gitlab::Access attr_accessor :raw_invite_token @@ -73,7 +74,7 @@ class Member < ActiveRecord::Base user end - def add_user(members, user_id, access_level, current_user = nil) + def add_user(members, user_id, access_level, current_user: nil, expires_at: nil) user = user_for_id(user_id) # `user` can be either a User object or an email to be invited @@ -87,6 +88,7 @@ class Member < ActiveRecord::Base if can_update_member?(current_user, member) || project_creator?(member, access_level) member.created_by ||= current_user member.access_level = access_level + member.expires_at = expires_at member.save end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 18e97c969d7..ec2d40eb11c 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -34,7 +34,7 @@ class ProjectMember < Member # :master # ) # - def add_users_to_projects(project_ids, user_ids, access, current_user = nil) + def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) @@ -50,7 +50,13 @@ class ProjectMember < Member project = Project.find(project_id) users.each do |user| - Member.add_user(project.project_members, user, access_level, current_user) + Member.add_user( + project.project_members, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5330a07ee35..62163e74000 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -10,14 +10,16 @@ class MergeRequest < ActiveRecord::Base belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :merge_user, class_name: "User" - has_one :merge_request_diff, dependent: :destroy + has_many :merge_request_diffs, dependent: :destroy + has_one :merge_request_diff, + -> { order('merge_request_diffs.id DESC') } has_many :events, as: :target, dependent: :destroy serialize :merge_params, Hash - after_create :create_merge_request_diff, unless: :importing? - after_update :update_merge_request_diff + after_create :ensure_merge_request_diff, unless: :importing? + after_update :reload_diff_if_branch_changed delegate :commits, :real_size, to: :merge_request_diff, prefix: nil @@ -89,13 +91,13 @@ class MergeRequest < ActiveRecord::Base end end - validates :source_project, presence: true, unless: [:allow_broken, :importing?] + validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches, unless: [:allow_broken, :importing?] - validate :validate_fork + validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] + validate :validate_fork, unless: :closed_without_fork? scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } @@ -170,10 +172,10 @@ class MergeRequest < ActiveRecord::Base end def diffs(diff_options = nil) - if self.compare - self.compare.diffs(diff_options) + if compare + compare.diffs(diff_options) else - Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + merge_request_diff.diffs(diff_options) end end @@ -184,8 +186,8 @@ class MergeRequest < ActiveRecord::Base def diff_base_commit if persisted? merge_request_diff.base_commit - elsif diff_start_commit && diff_head_commit - self.target_project.merge_base_commit(diff_start_sha, diff_head_sha) + else + branch_merge_base_commit end end @@ -246,6 +248,15 @@ class MergeRequest < ActiveRecord::Base target_project.repository.commit(target_branch) if target_branch_ref end + def branch_merge_base_commit + start_sha = target_branch_sha + head_sha = source_branch_sha + + if start_sha && head_sha + target_project.merge_base_commit(start_sha, head_sha) + end + end + def target_branch_sha @target_branch_sha || target_branch_head.try(:sha) end @@ -267,16 +278,16 @@ class MergeRequest < ActiveRecord::Base # Return diff_refs instance trying to not touch the git repository def diff_sha_refs if merge_request_diff && merge_request_diff.diff_refs_by_sha? - return Gitlab::Diff::DiffRefs.new( - base_sha: merge_request_diff.base_commit_sha, - start_sha: merge_request_diff.start_commit_sha, - head_sha: merge_request_diff.head_commit_sha - ) + merge_request_diff.diff_refs else diff_refs end end + def branch_merge_base_sha + branch_merge_base_commit.try(:sha) + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -294,36 +305,49 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project + return true if target_project == source_project + return true unless forked_source_project_missing? - if target_project == source_project - true - else - # If source and target projects are different - # we should check if source project is actually a fork of target project - if source_project.forked_from?(target_project) - true - else - errors.add :validate_fork, - 'Source project is not a fork of target project' - end - end + errors.add :validate_fork, + 'Source project is not a fork of the target project' end - def update_merge_request_diff + def closed_without_fork? + closed? && forked_source_project_missing? + end + + def forked_source_project_missing? + return false unless for_fork? + return true unless source_project + + !source_project.forked_from?(target_project) + end + + def ensure_merge_request_diff + merge_request_diff || create_merge_request_diff + end + + def create_merge_request_diff + merge_request_diffs.create + reload_merge_request_diff + end + + def reload_merge_request_diff + merge_request_diff(true) + end + + def reload_diff_if_branch_changed if source_branch_changed? || target_branch_changed? reload_diff end end def reload_diff - return unless merge_request_diff && open? + return unless open? old_diff_refs = self.diff_refs - - merge_request_diff.reload_content - + create_merge_request_diff MergeRequests::MergeRequestDiffCacheService.new.execute(self) - new_diff_refs = self.diff_refs update_diff_notes_positions( @@ -705,7 +729,9 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project + return unless diff_head_sha && source_project + + @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha) end def all_pipelines @@ -777,8 +803,12 @@ class MergeRequest < ActiveRecord::Base return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs? begin - @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines) - rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing + # Try to parse each conflict. If the MR's mergeable status hasn't been updated, + # ensure that we don't say there are conflicts to resolve when there are no conflict + # files. + conflicts.files.each(&:lines) + @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 + rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing @conflicts_can_be_resolved_in_ui = false end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 32cc6a3bfea..445179a4487 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -8,8 +8,6 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil - state_machine :state, initial: :empty do state :collected state :overflow @@ -24,12 +22,47 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_commits serialize :st_diffs - after_create :reload_content, unless: :importing? - after_save :keep_around_commits, unless: :importing? + # All diff information is collected from repository after object is created. + # It allows you to override variables like head_commit_sha before getting diff. + after_create :save_git_content, unless: :importing? + + def self.select_without_diff + select(column_names - ['st_diffs']) + end - def reload_content + # Collect information about commits and diff from repository + # and save it to the database as serialized data + def save_git_content + ensure_commits_sha + save_commits reload_commits - reload_diffs + save_diffs + keep_around_commits + end + + def ensure_commits_sha + merge_request.fetch_ref + self.start_commit_sha ||= merge_request.target_branch_sha + self.head_commit_sha ||= merge_request.source_branch_sha + self.base_commit_sha ||= find_base_sha + save + end + + # Override head_commit_sha to keep compatibility with merge request diff + # created before version 8.4 that does not store head_commit_sha in separate db field. + def head_commit_sha + if persisted? && super.nil? + last_commit.try(:sha) + else + super + end + end + + # This method will rely on repository branch sha + # in case start_commit_sha is nil. Its necesarry for old merge request diff + # created before version 8.4 to work + def safe_start_commit_sha + start_commit_sha || merge_request.target_branch_sha end def size @@ -38,14 +71,11 @@ class MergeRequestDiff < ActiveRecord::Base def raw_diffs(options = {}) if options[:ignore_whitespace_change] - @raw_diffs_no_whitespace ||= begin - compare = Gitlab::Git::Compare.new( + @diffs_no_whitespace ||= + Gitlab::Git::Compare.new( repository.raw_repository, - self.start_commit_sha || self.target_branch_sha, - self.head_commit_sha || self.source_branch_sha, - ) - compare.diffs(options) - end + safe_start_commit_sha, + head_commit_sha).diffs(options) else @raw_diffs ||= {} @raw_diffs[options] ||= load_diffs(st_diffs, options) @@ -56,6 +86,11 @@ class MergeRequestDiff < ActiveRecord::Base @commits ||= load_commits(st_commits || []) end + def reload_commits + @commits = nil + commits + end + def last_commit commits.first end @@ -65,55 +100,60 @@ class MergeRequestDiff < ActiveRecord::Base end def base_commit - return unless self.base_commit_sha + return unless base_commit_sha - project.commit(self.base_commit_sha) + project.commit(base_commit_sha) end def start_commit - return unless self.start_commit_sha + return unless start_commit_sha - project.commit(self.start_commit_sha) + project.commit(start_commit_sha) end def head_commit - return last_commit unless self.head_commit_sha + return unless head_commit_sha + + project.commit(head_commit_sha) + end + + def diff_refs + return unless start_commit_sha || base_commit_sha - project.commit(self.head_commit_sha) + Gitlab::Diff::DiffRefs.new( + base_sha: base_commit_sha, + start_sha: start_commit_sha, + head_sha: head_commit_sha + ) end def diff_refs_by_sha? base_commit_sha? && head_commit_sha? && start_commit_sha? end + def diffs(diff_options = nil) + Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options) + end + + def project + merge_request.target_project + end + def compare @compare ||= - begin - # Update ref for merge request - merge_request.fetch_ref + Gitlab::Git::Compare.new( + repository.raw_repository, + safe_start_commit_sha, + head_commit_sha + ) + end - Gitlab::Git::Compare.new( - repository.raw_repository, - self.target_branch_sha, - self.source_branch_sha - ) - end + def latest? + self == merge_request.merge_request_diff end private - # Collect array of Git::Commit objects - # between target and source branches - def unmerged_commits - commits = compare.commits - - if commits.present? - commits = Commit.decorate(commits, merge_request.source_project).reverse - end - - commits - end - def dump_commits(commits) commits.map(&:to_hash) end @@ -122,26 +162,21 @@ class MergeRequestDiff < ActiveRecord::Base array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } end - # Reload all commits related to current merge request from repo + # Load all commits related to current merge request diff from repo # and save it as array of hashes in st_commits db field - def reload_commits + def save_commits new_attributes = {} - commit_objects = unmerged_commits + commits = compare.commits - if commit_objects.present? - new_attributes[:st_commits] = dump_commits(commit_objects) + if commits.present? + commits = Commit.decorate(commits, merge_request.source_project).reverse + new_attributes[:st_commits] = dump_commits(commits) end update_columns_serialized(new_attributes) end - # Collect array of Git::Diff objects - # between target and source branches - def unmerged_diffs - compare.diffs(Commit.max_diff_options) - end - def dump_diffs(diffs) if diffs.respond_to?(:map) diffs.map(&:to_hash) @@ -162,16 +197,16 @@ class MergeRequestDiff < ActiveRecord::Base end end - # Reload diffs between branches related to current merge request from repo + # Load diffs between branches related to current merge request diff from repo # and save it as array of hashes in st_diffs db field - def reload_diffs + def save_diffs new_attributes = {} new_diffs = [] if commits.size.zero? new_attributes[:state] = :empty else - diff_collection = unmerged_diffs + diff_collection = compare.diffs(Commit.max_diff_options) if diff_collection.overflow? # Set our state to 'overflow' to make the #empty? and #collected? @@ -188,32 +223,17 @@ class MergeRequestDiff < ActiveRecord::Base end new_attributes[:st_diffs] = new_diffs - - new_attributes[:start_commit_sha] = self.target_branch_sha - new_attributes[:head_commit_sha] = self.source_branch_sha - new_attributes[:base_commit_sha] = branch_base_sha - update_columns_serialized(new_attributes) - - keep_around_commits - end - - def project - merge_request.target_project end def repository project.repository end - def branch_base_commit - return unless self.source_branch_sha && self.target_branch_sha - - project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) - end + def find_base_sha + return unless head_commit_sha && start_commit_sha - def branch_base_sha - branch_base_commit.try(:sha) + project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha) end def utf8_st_diffs @@ -248,8 +268,8 @@ class MergeRequestDiff < ActiveRecord::Base end def keep_around_commits - repository.keep_around(target_branch_sha) - repository.keep_around(source_branch_sha) - repository.keep_around(branch_base_sha) + repository.keep_around(start_commit_sha) + repository.keep_around(head_commit_sha) + repository.keep_around(base_commit_sha) end end diff --git a/app/models/note.rb b/app/models/note.rb index 3bbf5db0b70..b94e3cff2ce 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,6 +223,10 @@ class Note < ActiveRecord::Base end end + def user_authored?(user) + user == author + end + def award_emoji? can_be_award_emoji? && contains_emoji_only? end @@ -259,6 +263,8 @@ class Note < ActiveRecord::Base def ensure_discussion_id return unless self.persisted? + # Needed in case the SELECT statement doesn't ask for `discussion_id` + return unless self.has_attribute?(:discussion_id) return if self.discussion_id set_discussion_id diff --git a/app/models/project.rb b/app/models/project.rb index 043da030468..e5027af4a0e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -390,6 +390,13 @@ class Project < ActiveRecord::Base end end + def lfs_enabled? + return false unless Gitlab.config.lfs.enabled + return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? + + self[:lfs_enabled] + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage] end @@ -436,7 +443,7 @@ class Project < ActiveRecord::Base # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) - latest_pipeline = pipelines.latest_successful_for(ref).first + latest_pipeline = pipelines.latest_successful_for(ref) if latest_pipeline latest_pipeline.builds.latest.with_artifacts @@ -471,8 +478,6 @@ class Project < ActiveRecord::Base end def reset_cache_and_import_attrs - update(import_error: nil) - ProjectCacheWorker.perform_async(self.id) self.import_data.destroy if self.import_data @@ -611,7 +616,10 @@ class Project < ActiveRecord::Base end def new_issue_address(author) - if Gitlab::IncomingEmail.enabled? && author + # This feature is disabled for the time being. + return nil + + if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode Gitlab::IncomingEmail.reply_address( "#{path_with_namespace}+#{author.authentication_token}") end @@ -679,6 +687,10 @@ class Project < ActiveRecord::Base update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end + def has_wiki? + wiki_enabled? || has_external_wiki? + end + def external_wiki if has_external_wiki.nil? cache_has_external_wiki # Populate @@ -1003,8 +1015,8 @@ class Project < ActiveRecord::Base project_members.find_by(user_id: user) end - def add_user(user, access_level, current_user = nil) - team.add_user(user, access_level, current_user) + def add_user(user, access_level, current_user: nil, expires_at: nil) + team.add_user(user, access_level, current_user: current_user, expires_at: expires_at) end def default_branch @@ -1034,6 +1046,7 @@ class Project < ActiveRecord::Base "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) + repository.expire_avatar_cache(branch) reload_default_branch end @@ -1094,12 +1107,17 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def pipeline(sha, ref) + def pipeline_for(ref, sha = nil) + sha ||= commit(ref).try(:sha) + + return unless sha + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref, current_user = nil) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) + def ensure_pipeline(ref, sha, current_user = nil) + pipeline_for(ref, sha) || + pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index e52a6bd7c84..7613cbdea93 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -1,4 +1,6 @@ class ProjectGroupLink < ActiveRecord::Base + include Expirable + GUEST = 10 REPORTER = 20 DEVELOPER = 30 @@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base self.class.access_options.key(self.group_access) end - private + private def different_group if self.group && self.project && self.project.group == self.group diff --git a/app/models/project_team.rb b/app/models/project_team.rb index d0a714cd6fc..ab6ea2aae36 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -15,9 +15,9 @@ class ProjectTeam users, access, current_user = *args if users.respond_to?(:each) - add_users(users, access, current_user) + add_users(users, access, current_user: current_user) else - add_user(users, access, current_user) + add_user(users, access, current_user: current_user) end end @@ -33,17 +33,18 @@ class ProjectTeam member end - def add_users(users, access, current_user = nil) + def add_users(users, access, current_user: nil, expires_at: nil) ProjectMember.add_users_to_projects( [project.id], users, access, - current_user + current_user: current_user, + expires_at: expires_at ) end - def add_user(user, access, current_user = nil) - add_users([user], access, current_user) + def add_user(user, access, current_user: nil, expires_at: nil) + add_users([user], access, current_user: current_user, expires_at: expires_at) end # Remove all users from project team diff --git a/app/models/repository.rb b/app/models/repository.rb index 2494c266cd2..91bdafdac99 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -277,7 +277,7 @@ class Repository def cache_keys %i(size commit_count readme version contribution_guide changelog - license_blob license_key gitignore) + license_blob license_key gitignore koding_yml) end # Keys for data on branch/tag operations. @@ -553,6 +553,14 @@ class Repository end end + def koding_yml + return nil unless head_exists? + + cache.fetch(:koding_yml) do + file_on_head(/\A\.koding\.yml\z/) + end + end + def gitlab_ci_yml return nil unless head_exists? @@ -1057,7 +1065,7 @@ class Repository @avatar ||= cache.fetch(:avatar) do AVATAR_FILES.find do |file| - blob_at_branch('master', file) + blob_at_branch(root_ref, file) end end end diff --git a/app/models/todo.rb b/app/models/todo.rb index 8d7a5965aa1..6ae9956ade5 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,4 +1,6 @@ class Todo < ActiveRecord::Base + include Sortable + ASSIGNED = 1 MENTIONED = 2 BUILD_FAILED = 3 @@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base after_save :keep_around_commit + class << self + def sort(method) + method == "priority" ? order_by_labels_priority : order_by(method) + end + + # Order by priority depending on which issue/merge request the Todo belongs to + # Todos with highest priority first then oldest todos + # 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 + highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql + + select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). + order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). + order('todos.created_at') + end + end + def build_failed? action == BUILD_FAILED end diff --git a/app/models/user.rb b/app/models/user.rb index 48e83ab7e56..ad3cfbc03e4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -489,10 +489,10 @@ class User < ActiveRecord::Base (personal_projects.count.to_f / projects_limit) * 100 end - def recent_push(project_id = nil) + def recent_push(project_ids = nil) # Get push events not earlier than 2 hours ago events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) - events = events.where(project_id: project_id) if project_id + events = events.where(project_id: project_ids) if project_ids # Use the latest event that has not been pushed or merged recently events.recent.find do |event| diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 435a8c6e681..34efd09ed9f 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -36,7 +36,12 @@ module Boards end def set_state - params[:state] = list.done? ? 'closed' : 'opened' + params[:state] = + case list.list_type.to_sym + when :backlog then 'opened' + when :done then 'closed' + else 'all' + end end def board_label_ids diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 5cb408b9d20..b1887820bd4 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -3,7 +3,10 @@ module Boards class CreateService < Boards::BaseService def execute List.transaction do - create_list_at(next_position) + label = project.labels.find(params[:label_id]) + position = next_position + + create_list(label, position) end end @@ -14,8 +17,8 @@ module Boards max_position.nil? ? 0 : max_position.succ end - def create_list_at(position) - board.lists.create(params.merge(list_type: :label, position: position)) + def create_list(label, position) + board.lists.create(label: label, list_type: :label, position: position) end end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 6f7610d42ba..f049ed628db 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -34,7 +34,7 @@ module Ci end def process_build(build, current_status) - return false unless Statuseable::COMPLETED_STATUSES.include?(current_status) + return false unless HasStatus::COMPLETED_STATUSES.include?(current_status) if valid_statuses_for_when(build.when).include?(current_status) build.enqueue diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb deleted file mode 100644 index 92e6df442b4..00000000000 --- a/app/services/ci/web_hook_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Ci - class WebHookService - def build_end(build) - execute_hooks(build.project, build_data(build)) - end - - def execute_hooks(project, data) - project.web_hooks.each do |web_hook| - async_execute_hook(web_hook, data) - end - end - - def async_execute_hook(hook, data) - Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) - end - - def build_data(build) - project = build.project - data = {} - data.merge!({ - build_id: build.id, - build_name: build.name, - build_status: build.status, - build_started_at: build.started_at, - build_finished_at: build.finished_at, - project_id: project.id, - project_name: project.name, - gitlab_url: project.gitlab_url, - ref: build.ref, - before_sha: build.before_sha, - sha: build.sha, - }) - end - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e06c37c323e..4c8d93999a7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,7 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:labels) params.delete(:add_label_ids) params.delete(:remove_label_ids) params.delete(:label_ids) @@ -72,6 +73,7 @@ class IssuableBaseService < BaseService filter_labels_in_param(:add_label_ids) filter_labels_in_param(:remove_label_ids) filter_labels_in_param(:label_ids) + find_or_create_label_ids end def filter_labels_in_param(key) @@ -80,6 +82,17 @@ class IssuableBaseService < BaseService params[key] = project.labels.where(id: params[key]).pluck(:id) end + def find_or_create_label_ids + labels = params.delete(:labels) + return unless labels + + params[:label_ids] = labels.split(",").map do |label_name| + project.labels.create_with(color: Label::DEFAULT_COLOR) + .find_or_create_by(title: label_name.strip) + .id + end + end + def process_label_ids(attributes, existing_label_ids: nil) label_ids = attributes.delete(:label_ids) add_label_ids = attributes.delete(:add_label_ids) @@ -162,7 +175,12 @@ class IssuableBaseService < BaseService if params.present? && update_issuable(issuable, params) issuable.reset_events_cache - handle_common_system_notes(issuable, old_labels: old_labels) + + # We do not touch as it will affect a update on updated_at field + ActiveRecord::Base.no_touching do + handle_common_system_notes(issuable, old_labels: old_labels) + end + handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb new file mode 100644 index 00000000000..ca9db59cac7 --- /dev/null +++ b/app/services/members/authorized_destroy_service.rb @@ -0,0 +1,19 @@ +module Members + class AuthorizedDestroyService < BaseService + attr_accessor :member, :user + + def initialize(member, user = nil) + @member, @user = member, user + end + + def execute + return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) + + member.destroy + + if member.request? && member.user != user + notification_service.decline_access_request(member) + end + end + end +end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9e3f6af628d..9a2bf82ef51 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -11,12 +11,7 @@ module Members unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise Gitlab::Access::AccessDeniedError end - - member.destroy - - if member.request? && member.user != current_user - notification_service.decline_access_request(member) - end + AuthorizedDestroyService.new(member, current_user).execute end end end diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index adc71b0c2bc..19caa038c44 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,11 +1,14 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - attr_accessor :conflicts, :rugged, :merge_index + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @conflicts = merge_request.conflicts @rugged = project.repository.rugged @merge_index = conflicts.merge_index + @merge_request = merge_request + + fetch_their_commit! conflicts.files.each do |file| write_resolved_file_to_index(file, params[:sections]) @@ -27,5 +30,21 @@ module MergeRequests merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) merge_index.conflict_remove(our_path) end + + # If their commit (in the target project) doesn't exist in the source project, it + # can't be a parent for the merge commit we're about to create. If that's the case, + # fetch the target branch ref into the source project so the commit exists in both. + # + def fetch_their_commit! + return if rugged.include?(conflicts.their_commit.oid) + + random_string = SecureRandom.hex + + project.repository.fetch_ref( + merge_request.target_project.repository.path_to_repo, + "refs/heads/#{merge_request.target_branch}", + "refs/tmp/#{random_string}/head" + ) + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 30c5f24988c..398ec47f0ea 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,10 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + if merge_request.closed_without_fork? + params.except!(:target_branch, :force_remove_source_branch) + end + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) update(merge_request) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 546a8f11330..0c8446e7c3d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -269,11 +269,11 @@ module SystemNoteService # # Example Note text: # - # "mentioned in #1" + # "Mentioned in #1" # - # "mentioned in !2" + # "Mentioned in !2" # - # "mentioned in 54f7727c" + # "Mentioned in 54f7727c" # # See cross_reference_note_content. # @@ -308,7 +308,7 @@ module SystemNoteService # Check if a cross-reference is disallowed # - # This method prevents adding a "mentioned in !1" note on every single commit + # This method prevents adding a "Mentioned in !1" note on every single commit # in a merge request. Additionally, it prevents the creation of references to # external issues (which would fail). # @@ -417,7 +417,7 @@ module SystemNoteService end def cross_reference_note_prefix - 'mentioned in ' + 'Mentioned in ' end def cross_reference_note_content(gfm_reference) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e0ccb654590..2aab8c736d6 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -148,7 +148,8 @@ class TodoService def mark_todos_as_done_by_ids(ids, current_user) todos = current_user.todos.where(id: ids) - marked_todos = todos.update_all(state: :done) + # Only return those that are not really on that state + marked_todos = todos.where.not(state: :done).update_all(state: :done) current_user.update_todos_count_cache marked_todos end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 92e2dae4842..9175b3d3f96 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -13,7 +13,7 @@ .col-sm-10 = f.text_area :description, class: "form-control", rows: 10 .hint - Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}. + Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. .form-group = f.label :logo, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c7fd344eea2..d929364fc96 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -388,6 +388,25 @@ .help-block If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + %fieldset + %legend Koding + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :koding_enabled do + = f.check_box :koding_enabled + Enable Koding + .form-group + = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' + .help-block + Koding has integration enabled out of the box for the + %strong gitlab + team, and you need to provide that team's URL here. Learn more in the + = succeed "." do + = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 89d7a40d6b0..107fc25244a 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -1,22 +1,24 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 352adbedee4..f29d9c94441 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -51,7 +51,7 @@ - if build.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(build.finished_at, build.started_at) + = duration_in_numbers(build.duration) - if build.finished_at %p.finished-at diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index b74da64f82e..c91ab4cb946 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -1,26 +1,28 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Overview - = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do - %span - Builds - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Overview + = nav_link(controller: [:admin, :projects]) do + = link_to admin_namespaces_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'builds#index' do + = link_to admin_builds_path, title: 'Builds' do + %span + Builds + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index b2c607361b3..6c7c3c48604 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,6 +73,12 @@ %span.light last commit: %strong = last_commit(@project) + + %li + %span.light Git LFS status: + %strong + = project_lfs_status(@project) + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - else %li %span.light repository: diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index 6956e5ab795..bfc6142067a 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -9,12 +9,20 @@ .light-well %h4 CPU .data - %h1= "#{@cpus} cores" + - if @cpus + %h1= "#{@cpus.length} cores" + - else + = icon('warning', class: 'text-warning') + Unable to collect CPU info .col-sm-4 .light-well %h4 Memory .data - %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}" + - if @memory + %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}" + - else + = icon('warning', class: 'text-warning') + Unable to collect memory info .col-sm-4 .light-well %h4 Disks diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 0044d779c31..889086c62b1 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,3 +1,6 @@ +- page_title "CI Lint" +- page_description "Validate your GitLab CI configuration file" + %h2 Check your .gitlab-ci.yml %hr diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 4e340b6ec16..d320d3bcc1e 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -43,6 +43,25 @@ class: 'select2 trigger-submit', include_blank: true, data: {placeholder: 'Action'}) + .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 + %b.caret + %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 - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index b2e55f7647a..3a95a652810 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -7,7 +7,7 @@ .diff-content.code.js-syntax-highlight %table - - discussions = { discussion.line_code => discussion } + - discussions = { discussion.original_line_code => discussion } = render partial: "projects/diffs/line", collection: discussion.truncated_diff_lines, as: :line, diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 57f6e7e0612..b8248a80a27 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -24,7 +24,7 @@ - else = sort_title_recently_created %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li = link_to explore_groups_path(sort: sort_value_recently_created) do = sort_title_recently_created diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 9bb9f962177..2fb3190ab11 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -14,5 +14,14 @@ Read more about role permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .form-group + = f.label :expires_at, 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, the user(s) will automatically lose access to this group and all of its projects. + .form-actions = f.submit 'Add users to group', class: "btn btn-create" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 90f362c052b..f789796e942 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -17,7 +17,7 @@ .panel-heading %strong #{@group.name} group members - %span.badge= @members.size + %span.badge= @members.total_count .controls = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index da71de4cd1e..742f9d7a433 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,3 @@ :plain $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); + new MemberExpirationDate(); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 85e188d6f8b..d16bd61b779 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -549,4 +549,4 @@ %li wiki page %li help page - You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}. + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}. diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml deleted file mode 100644 index ed3afb0ce33..00000000000 --- a/app/views/import/gitorious/status.html.haml +++ /dev/null @@ -1,54 +0,0 @@ -- page_title "Gitorious import" -- header_title "Projects", root_path -%h3.page-title - %i.icon-gitorious.icon-gitorious-big - Import projects from Gitorious.org - -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From Gitorious.org - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank" - %td.import-target - = repo.full_name - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } } diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml new file mode 100644 index 00000000000..65887aacbaf --- /dev/null +++ b/app/views/koding/index.html.haml @@ -0,0 +1,6 @@ +.row-content-block.second-block.center + %p + = icon('circle', class: 'cgreen') + Integration is active for + = link_to koding_project_url, target: '_blank' do + #{current_application_settings.koding_url} diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml new file mode 100644 index 00000000000..22319bba745 --- /dev/null +++ b/app/views/layouts/koding.html.haml @@ -0,0 +1,5 @@ +- page_title "Koding" +- page_description "Koding Dashboard" +- header_title "Koding", koding_path + += render template: "layouts/application" diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 3a14751ea8e..67f558c854b 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -12,6 +12,11 @@ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do %span Activity + - if koding_enabled? + = nav_link(controller: :koding) do + = link_to koding_path, title: 'Koding' do + %span + Koding = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 1d3b8fc3683..f7012595a5a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -65,7 +65,7 @@ Graphs - if project_nav_tab? :issues - = nav_link(controller: [:issues, :labels, :milestones]) do + = nav_link(controller: [:issues, :labels, :milestones, :boards]) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do %span Issues diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 7b0621f9401..680e95ac6b5 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,4 +1,7 @@ - page_title "Edit", @blob.path, @ref +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') - if @conflict .alert.alert-danger @@ -16,14 +19,10 @@ = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) - -:javascript - blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") - new NewCommitForm($('.js-edit-blob-form')) diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index c952bc7e5db..b6ed9518c48 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,17 +1,16 @@ - page_title "New File", @path.presence, @ref +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') %h3.page-title New File .file-editor - = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref = render 'shared/new_commit_form', placeholder: "Add new file" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) - -:javascript - blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}") - new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index f8ebf397ee2..6b4bfe0c354 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -11,7 +11,6 @@ .board-inner %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } - = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)") {{ list.title }} %span.pull-right{ "v-if" => "list.type !== 'blank'" } {{ list.issues.length }} @@ -22,11 +21,6 @@ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } = icon("trash") = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore") - .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" } - %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" } - = icon("search", class: "board-search-icon", "v-show" => "!query") - %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" } - = icon("times", class: "board-search-clear") %board-list{ "inline-template" => true, "v-if" => "list.type !== 'blank'", ":list" => "list", diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index b20c23f6b8e..e8b60b54d80 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -9,7 +9,6 @@ "track-by" => "id" } %li.card{ ":class" => "{ 'user-can-drag': !disabled }", ":index" => "index" } - = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled") %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") %a{ ":href" => "issueLinkBase + '/' + issue.id", diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 6192ccb710b..808e6b95746 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,6 +27,8 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do Compare + = render 'projects/buttons/download', project: @project, ref: branch.name + - if can_remove_branch?(@project, branch.name) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 58f43ecb5d5..5f5e071eb40 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,42 @@ -- unless @project.empty_repo? - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do - = icon('download') +- if !project.empty_repo? && can?(current_user, :download_code, project) + %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'} + .dropdown.inline + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + %span.caret + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar + + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml new file mode 100644 index 00000000000..fdc80d44253 --- /dev/null +++ b/app/views/projects/buttons/_koding.html.haml @@ -0,0 +1,7 @@ +- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch) + - if @repository.koding_yml + = link_to koding_project_url(@project), class: 'btn', target: '_blank' do + Run in IDE (Koding) + - else + = link_to add_koding_stack_path(@project), class: 'btn' do + Set Up Koding diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 91081435220..1fdf32466f2 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -63,7 +63,7 @@ - if build.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(build.finished_at, build.started_at) + = duration_in_numbers(build.duration) - if build.finished_at %p.finished-at = icon("calendar") diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 04cbd0c3591..36fb0300aeb 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,14 +1,15 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) %li.build{class: ("playable" if is_playable)} + .curve .build-content - if is_playable = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do = render_status_with_link('build', 'play') - = subject.name + %span.ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do = render_status_with_link('build', subject.status) - = subject.name + %span.ci-status-text= subject.name - else = render_status_with_link('build', 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 be387201f8d..b119f6edf14 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,23 +1,23 @@ - status = pipeline.status %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + = 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) %td .branch-commit - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do %span ##{pipeline.id} - if pipeline.ref - unless defined?(hide_branch) && hide_branch .icon-container = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" .icon-container = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - if pipeline.latest? %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - if pipeline.triggered? @@ -30,7 +30,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(@project.namespace, @project, commit.id), class: "commit-row-message" + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - else Cant find HEAD commit for this branch @@ -41,17 +41,17 @@ - status = stages_status[stage] - tooltip = "#{stage.titleize}: #{status || 'not found'}" - if status - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do = ci_icon_for_status(status) - else .light.has-tooltip{ title: tooltip } \- %td - - if pipeline.started_at && pipeline.finished_at + - if pipeline.duration %p.duration = custom_icon("icon_timer") - = duration_in_numbers(pipeline.finished_at, pipeline.started_at) + = duration_in_numbers(pipeline.duration) - if pipeline.finished_at %p.finished-at = icon("calendar") @@ -71,7 +71,7 @@ %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li - = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do + = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = icon("play") %span= build.name.humanize - if artifacts.present? @@ -82,15 +82,15 @@ %ul.dropdown-menu.dropdown-menu-align-right - artifacts.each do |build| %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do = icon("download") %span Download '#{build.name}' artifacts - - if can?(current_user, :update_pipeline, @project) + - if can?(current_user, :update_pipeline, pipeline.project) .cancel-retry-btns.inline - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do = icon("repeat") - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do = icon("remove") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index d9b800a4ded..e4cd55b9f7a 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -17,7 +17,9 @@ .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 - = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch" + = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }}) + - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml index 9d925cacc0d..6bb900e3fc1 100644 --- a/app/views/projects/commit/_ci_stage.html.haml +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -8,8 +8,8 @@ - if stage = stage.titleize - = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true - = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true + = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true %tr %td{colspan: 10} diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 14adee7a6e9..29d767e7769 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -58,9 +58,8 @@ = ci_icon_for_status(@commit.status) %span.ci-status-label = ci_label_for_status(@commit.status) - - if @commit.pipelines.duration - in - = time_interval_in_words @commit.pipelines.duration + in + = time_interval_in_words @commit.pipelines.total_duration .commit-box.content-block %h3.commit-title diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 29f4ef8f49e..f41a11a056d 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -10,7 +10,10 @@ %th Commit - pipelines.stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 61152649907..4d1ee1c5318 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,8 +1,5 @@ .scrolling-tabs-container.sub-nav-scroll - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') + = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b282aa52b25..836c6d7b83f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -80,6 +80,16 @@ %strong Snippets %br %span.descr Share code pastes with others out of git repository + - if Gitlab.config.lfs.enabled && current_user.admin? + .form-group + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled, checked: @project.lfs_enabled? + %strong LFS + %br + %span.descr + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - if Gitlab.config.registry.enabled .form-group .checkbox 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 584c0fa18ae..31d40f6ad03 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 @@ -3,7 +3,7 @@ - if subject.target_url - link_to subject.target_url do = render_status_with_link('commit status', subject.status) - = subject.name + %span.ci-status-text= subject.name - else = render_status_with_link('commit status', subject.status) - = subject.name + %span.ci-status-text= subject.name diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 45e51389c00..a231d684559 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,16 +1,18 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } - - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/chart.js') - = page_specific_javascript_tag('graphs/graphs_bundle.js') - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.builds_enabled? - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/chart.js') + = page_specific_javascript_tag('graphs/graphs_bundle.js') + = nav_link(action: :show) do + = link_to 'Contributors', namespace_project_graph_path + = nav_link(action: :commits) do + = link_to 'Commits', commits_namespace_project_graph_path + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.builds_enabled? + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index 2b904544f28..ca700cb3a3b 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -17,6 +17,13 @@ .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" %span.caret + .form-group + = label_tag :expires_at, 'Access expiration date', class: 'label-light' + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, all users in the group will automatically lose access to this project. = submit_tag "Share", class: "btn btn-create" .col-lg-9.col-lg-offset-3 %hr @@ -35,6 +42,10 @@ = group.name %br up to #{group_link.human_access} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .pull-right = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do %span.sr-only disable sharing diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index b6cb559afcb..f88b33018d0 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,30 +1,32 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) - = nav_link(controller: :issues) do - = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do - %span - Issues +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) + = nav_link(controller: :issues) do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do + %span + Issues - = nav_link(controller: :boards) do - = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do - %span - Board + = nav_link(controller: :boards) do + = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do + %span + Board - - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do - %span - Merge Requests + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - %span - Labels + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - %span - Milestones + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones
\ No newline at end of file diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 6ea9f612d13..a8eeab3e55e 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,7 +5,7 @@ - @related_branches.each do |branch| %li - target = @project.repository.find_branch(branch).target - - pipeline = @project.pipeline(target.sha, branch) if target + - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f8025fc1dbe..9d8b4cc56be 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -16,6 +16,9 @@ - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? + - if koding_enabled? && @repository.koding_yml + = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do + Run in IDE (Koding) = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 013b05628fa..99c71e1454a 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,4 +1,5 @@ - if @merge_request_diff.collected? + = render 'projects/merge_requests/show/versions' = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 098ce19da21..e35291dff7d 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,3 +1,7 @@ +- if @merge_request.closed_without_fork? + .alert.alert-danger + %p The source project of this merge request has been removed. + .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box{ class: status_box_class(@merge_request) } diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml new file mode 100644 index 00000000000..2da70ce7137 --- /dev/null +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -0,0 +1,31 @@ +- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff + +- if merge_request_diffs.size > 1 + .mr-version-switch + Version: + %span.dropdown.inline + %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} } + %strong.monospace< + - if @merge_request_diff.latest? + #{"latest"} + - else + #{@merge_request_diff.head_commit.short_id} + %span.caret + %ul.dropdown-menu.dropdown-menu-selectable + - merge_request_diffs.each do |merge_request_diff| + %li + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do + %strong.monospace + #{merge_request_diff.head_commit.short_id} + %br + %small + #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + = time_ago_with_tooltip(merge_request_diff.created_at) + + - unless @merge_request_diff.latest? + %span.prepend-left-default + = icon('info-circle') + This version is not the latest one. Comments are disabled + .pull-right + %span.monospace + #{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id} diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index ea4898f2107..0a1e2bb2cc6 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -60,11 +60,6 @@ - unless gitlab_import_configured? = render 'gitlab_import_modal' %div - - if gitorious_import_enabled? - = link_to new_import_gitorious_path, class: 'btn import_gitorious' do - %i.icon-gitorious.icon-gitorious-small - Gitorious.org - %div - if google_code_import_enabled? = link_to new_import_google_code_path, class: 'btn import_google_code' do = icon('google', text: 'Google Code') diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index cf6e14648cc..6c14f48d41b 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -2,10 +2,10 @@ .comment-toolbar.clearfix .toolbar-text Styling with - = link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1 + = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 - if supports_slash_commands and - = link_to 'slash commands', help_page_path('workflow/slash_commands'), target: '_blank', tabindex: -1 + = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 are - else is diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index d2ac1ce2b9a..7c82177f9ea 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -52,11 +52,11 @@ - if note.emoji_awardable? = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') - = icon('smile-o') + = icon('smile-o', class: 'link-highlight') - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do - = icon('pencil') + = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = icon('trash-o') .note-body{class: note_editable ? 'js-task-list-container' : ''} diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index d65faf86d4e..f611ddc8f5f 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,19 +1,21 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - %span - Builds + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds - - if project_nav_tab? :environments - = nav_link(controller: %w(environments)) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do - %span - Environments + - if project_nav_tab? :environments + = nav_link(controller: %w(environments)) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 8289aefcde7..063e83a407a 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -9,7 +9,7 @@ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" - if @pipeline.duration in - = time_interval_in_words @pipeline.duration + = time_interval_in_words(@pipeline.duration) .pull-right = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 5f466bdbac2..4d957e0d890 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -49,7 +49,10 @@ %th Commit - stages.each do |stage| %th.stage - %span.has-tooltip{ title: "#{stage.titleize}" } + - if stage.titleize.length > 12 + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + - else = stage.titleize %th %th diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 978c4dfc5ec..fa8cbf71733 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -14,5 +14,14 @@ Read more about role permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .form-group + = f.label :expires_at, 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .help-block + On this date, the user(s) will automatically lose access to this project. + .form-actions = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9031f01b496..9d063b3081f 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,6 +1,6 @@ - page_title "Members" -.project-members-page.prepend-top-default +.project-members-page.js-project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 45f8ef89060..833954bc039 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,2 +1,3 @@ :plain $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); + new MemberExpirationDate(); diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index d4c6fa24768..e95a3b1b4c3 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -22,16 +22,20 @@ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } Allowed to merge: .col-md-10 - = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-merge wide', - data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) + .merge_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-merge wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) .form-group %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } Allowed to push: .col-md-10 - = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-push wide', - data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) + .push_access_levels-container + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-push wide', + dropdown_class: 'dropdown-menu-selectable', + data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 835398b6f98..33d5cbff420 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,18 +1,20 @@ +- @no_container = true - page_title "Edit", @tag.name, "Tags" = render "projects/commits/head" -.row-content-block - .oneline - .title - Release notes for tag - %strong #{@tag.name} +%div{ class: container_class } + .sub-header-block.no-bottom-space + .oneline + .title + Release notes for tag + %strong #{@tag.name} + -.prepend-top-default = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' .error-alert - .form-actions.prepend-top-default + .prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml deleted file mode 100644 index 24658319060..00000000000 --- a/app/views/projects/repositories/_download_archive.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- ref = ref || nil -- btn_class = btn_class || '' -- split_button = split_button || false -- if split_button == true - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar -- else - %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span zip - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do - %i.fa.fa-download - %span tar.gz diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a666d07e9eb..9adce776c1c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -64,13 +64,15 @@ %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do Set Up CI + %li.project-repo-buttons-right .project-repo-buttons.project-right-buttons - if current_user = render 'shared/members/access_request_buttons', source: @project + = render "projects/buttons/koding" .btn-group.project-repo-btn-group - = render "projects/buttons/download" + = render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/dropdown' = render 'shared/notifications/button', notification_setting: @notification_setting @@ -86,4 +88,4 @@ Archived project! Repository is read-only %div{class: "project-show-#{default_project_view}"} - = render default_project_view
\ No newline at end of file + = render default_project_view diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml deleted file mode 100644 index 8a11dbfa9f4..00000000000 --- a/app/views/projects/tags/_download.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%span.btn-group - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %span Source code - %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } - %span.caret - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2c11c0e5b21..a156d98bab8 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,8 +11,7 @@ = strip_gpg_signature(tag.message) .controls - - if can?(current_user, :download_code, @project) - = render 'projects/tags/download', ref: tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: tag.name - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 395d7af6cbb..4dd7439b2d0 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -12,8 +12,7 @@ = icon('files-o') = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do = icon('history') - - if can? current_user, :download_code, @project - = render 'projects/tags/download', ref: @tag.name, project: @project + = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 558e6146ae9..ca5d2d7722a 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -5,16 +5,17 @@ %tr %th Name %th Last Update - %th.hidden-xs - .pull-left Last Commit - .last-commit.hidden-sm.pull-left - + %th.hidden-xs.last-commit + Last Commit + .last-commit-content.hidden-sm %i.fa.fa-angle-right %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – - = truncate(@commit.title, length: 50) + = time_ago_with_tooltip(@commit.committed_date) + – + = @commit.full_title = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - if @path.present? diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index bf5360b4dee..37d341212af 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -10,8 +10,7 @@ %div{ class: container_class } .tree-controls = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true + = render 'projects/buttons/download', project: @project, ref: @ref #tree-holder.tree-holder.clearfix .nav-block diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index f8ea479e0b1..551a20c1044 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,13 +1,15 @@ -.nav-links.sub-nav - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) +.scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access + = nav_link(path: 'wikis#git_access') do + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + Git Access - = render 'projects/wikis/new' + = render 'projects/wikis/new' diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index b07f1c5603e..9b67422da2c 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,9 +1,9 @@ -<svg width="36" height="36" id="tanuki-logo"> - <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> - <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> - <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> - <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/> - <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/> - <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/> - <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/> +<svg width="36" height="36" class="tanuki-logo"> + <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> + <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> + <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> + <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/> + <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/> + <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/> + <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/> </svg> diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml new file mode 100644 index 00000000000..4e3b1b3a571 --- /dev/null +++ b/app/views/shared/_nav_scroll.html.haml @@ -0,0 +1,4 @@ +.fade-left + = icon('angle-left') +.fade-right + = icon('angle-right')
\ No newline at end of file diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index ea7162d4d63..9a8252ab087 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,7 +6,7 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } = dropdown_title "Switch branch/tag" = dropdown_filter "Search branches and tags" diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg new file mode 100644 index 00000000000..80a6d41dbf6 --- /dev/null +++ b/app/views/shared/icons/_icon_play.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg new file mode 100644 index 00000000000..1f5c3b51b03 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 4f8ea7e7cef..0f4f744a71f 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -27,15 +27,18 @@ = render "shared/issuable/label_dropdown" .pull-right - - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project) - .dropdown - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } - Create new list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading + - if controller.controller_name == 'boards' + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading - else = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d717c3d92ee..3856a4917b4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,5 +1,12 @@ = form_errors(issuable) +- if @conflict + .alert.alert-danger + Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. + Please check out + = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank" + and make sure your changes will not unintentionally remove theirs + .form-group = f.label :title, class: 'control-label' @@ -42,7 +49,7 @@ - if can_add_template?(issuable) %p.help-block Add - = link_to "description templates", help_page_path('user/project/description_templates') + = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1 to help your contributors communicate effectively! .form-group.detail-page-description @@ -121,13 +128,13 @@ = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 .issuable-form-select-holder - = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) } + = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE } %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } = icon('question-circle') -- if issuable.is_a?(MergeRequest) +- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork? %hr - if @merge_request.new_record? .form-group @@ -168,7 +175,9 @@ = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' + += f.hidden_field :lock_version diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index a76b7baf918..c0dc63be2bf 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -8,7 +8,10 @@ - if show_boards_content .issue-board-dropdown-content %p - Each label that exists in your issue tracker can have its own dedicated list. Select a label below to add a list to your Board and it will automatically be populated with issues that have that label. To create a list for a label that doesn't exist yet, simply create the label below. + Each label that exists in your issue tracker can have its own dedicated + list. Select a label below to add a list to your Board and it will + automatically be populated with issues that have that label. To create + a list for a label that doesn't exist yet, simply create the label below. = dropdown_filter(filter_placeholder) = dropdown_content - if @project && show_footer diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 8e2fcbdfab8..c1b50e65af5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -109,7 +109,7 @@ - if issuable.project.labels.any? .block.labels - .sidebar-collapsed-icon + .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } = icon('tags') %span = issuable.labels_array.size diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index fc6e206d082..5f20e4bd42a 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -16,7 +16,7 @@ = button_tag icon('pencil'), type: 'button', class: 'btn inline js-toggle-button', - title: 'Edit access level' + title: 'Edit' - if member.request? = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), @@ -59,6 +59,10 @@ = time_ago_with_tooltip(member.requested_at) - else Joined #{time_ago_with_tooltip(member.created_at)} + - if member.expires? + · + %span{ class: ('text-warning' if member.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' @@ -73,8 +77,16 @@ - if show_roles .edit-member.hide.js-toggle-content %br - = form_for member, remote: true do |f| - .prepend-top-10 - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control' + = form_for member, remote: true, html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' + .col-sm-10 + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" + .form-group + = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}" + %i.clear-icon.js-clear-input .prepend-top-10 = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 47ec09f62c6..0c788032020 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,3 +1,7 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + = page_specific_javascript_tag('snippet/snippet_bundle.js') + .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| = form_errors(@snippet) @@ -31,8 +35,3 @@ - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" -:javascript - var editor = ace.edit("editor"); - $(".snippet-form-holder form").submit(function(){ - $(".snippet-file-content").val(editor.getValue()); - }); diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index c6a5af2809a..1dc7e0adef7 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,13 +33,13 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new.execute(project, before_sha, project, after_sha) + compare = CompareService.new.execute(project, after_sha, project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new.execute(project, after_sha, project, before_sha) + compare = CompareService.new.execute(project, before_sha, project, after_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb new file mode 100644 index 00000000000..246c8b6650a --- /dev/null +++ b/app/workers/remove_expired_group_links_worker.rb @@ -0,0 +1,7 @@ +class RemoveExpiredGroupLinksWorker + include Sidekiq::Worker + + def perform + ProjectGroupLink.expired.destroy_all + end +end diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb new file mode 100644 index 00000000000..cf765af97ce --- /dev/null +++ b/app/workers/remove_expired_members_worker.rb @@ -0,0 +1,13 @@ +class RemoveExpiredMembersWorker + include Sidekiq::Worker + + def perform + Member.expired.find_each do |member| + begin + Members::AuthorizedDestroyService.new(member).execute + rescue => ex + logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") + end + end + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index e6701078f71..d2ca8813ab9 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -14,6 +14,8 @@ class RepositoryImportWorker import_url: @project.import_url, path: @project.path_with_namespace) + project.update_column(:import_error, nil) + result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error diff --git a/config/application.rb b/config/application.rb index 6b80f8ddafa..4792f6670a8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -88,6 +88,8 @@ module Gitlab config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" + config.assets.precompile << "blob_edit/blob_edit_bundle.js" + config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index deac3b0f0f9..4a01b9e40fb 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -212,7 +212,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['domain_whitelist'] ||= [] -Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project] +Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project] Settings.gitlab['trusted_proxies'] ||= [] # @@ -293,6 +293,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' +Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *' +Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker' +Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' +Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker' # # GitLab Shell diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb new file mode 100644 index 00000000000..0da584626ee --- /dev/null +++ b/config/initializers/ar_monkey_patch.rb @@ -0,0 +1,57 @@ +# rubocop:disable Lint/RescueException + +# This patch fixes https://github.com/rails/rails/issues/26024 +# TODO: Remove it when it's no longer necessary + +module ActiveRecord + module Locking + module Optimistic + # We overwrite this method because we don't want to have default value + # for newly created records + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + super + end + + def _update_record(attribute_names = self.attribute_names) #:nodoc: + return super unless locking_enabled? + return 0 if attribute_names.empty? + + lock_col = self.class.locking_column + + previous_lock_value = send(lock_col).to_i + + # This line is added as a patch + previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 + + increment_lock + + attribute_names += [lock_col] + attribute_names.uniq! + + begin + relation = self.class.unscoped + + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attributes_for_update(attribute_names).map do |name| + [name, _read_attribute(name)] + end.to_h + ) + + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError.new(self, "update") + end + + affected_rows + + # If something went wrong, revert the version. + rescue Exception + send(lock_col + '=', previous_lock_value) + raise + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 66f77aee06a..262a174437a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,11 @@ Rails.application.routes.draw do get 'help/*path' => 'help#show', as: :help_page # + # Koding route + # + get 'koding' => 'koding#index' + + # # Global snippets # resources :snippets do @@ -152,12 +157,6 @@ Rails.application.routes.draw do get :jobs end - resource :gitorious, only: [:create, :new], controller: :gitorious do - get :status - get :callback - get :jobs - end - resource :google_code, only: [:create, :new], controller: :google_code do get :status post :callback @@ -783,6 +782,14 @@ Rails.application.routes.draw do resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all + + resources :artifacts, only: [] do + collection do + get :latest_succeeded, + path: '*ref_name_and_path', + format: false + end + end end member do diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_pipelines.rb index 069d9dd6226..49e6e2361b1 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -1,4 +1,4 @@ -class Gitlab::Seeder::Builds +class Gitlab::Seeder::Pipelines STAGES = %w[build test deploy notify] BUILDS = [ { name: 'build:linux', stage: 'build', status: :success }, @@ -7,11 +7,12 @@ class Gitlab::Seeder::Builds { name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:windows', stage: 'test', status: :success }, { name: 'rspec:osx', stage: 'test', status_event: :success }, - { name: 'spinach:linux', stage: 'test', status: :pending }, - { name: 'spinach:osx', stage: 'test', status: :canceled }, - { name: 'cucumber:linux', stage: 'test', status: :running }, - { name: 'cucumber:osx', stage: 'test', status: :failed }, - { name: 'staging', stage: 'deploy', environment: 'staging', status: :success }, + { name: 'spinach:linux', stage: 'test', status: :success }, + { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true}, + { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending }, + { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running }, + { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled }, + { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success }, { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped }, { name: 'slack', stage: 'notify', when: 'manual', status: :created }, ] @@ -34,72 +35,86 @@ class Gitlab::Seeder::Builds end end + private + def pipelines - master_pipelines + merge_request_pipelines + create_master_pipelines + create_merge_request_pipelines end - def master_pipelines - create_pipelines_for(@project, 'master') + def create_master_pipelines + @project.repository.commits('master', limit: 4).map do |commit| + create_pipeline!(@project, 'master', commit) + end rescue [] end - def merge_request_pipelines - @project.merge_requests.last(5).map do |merge_request| - create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5)) - end.flatten + def create_merge_request_pipelines + pipelines = @project.merge_requests.first(3).map do |merge_request| + project = merge_request.source_project + branch = merge_request.source_branch + + merge_request.commits.last(4).map do |commit| + create_pipeline!(project, branch, commit) + end + end + + pipelines.flatten rescue [] end - def create_pipelines_for(project, ref) - commits = project.repository.commits(ref, limit: 5) - create_pipelines(project, ref, commits) + + def create_pipeline!(project, ref, commit) + project.pipelines.create(sha: commit.id, ref: ref) end - def create_pipelines(project, ref, commits) - commits.map do |commit| - project.pipelines.create(sha: commit.id, ref: ref) + def build_create!(pipeline, opts = {}) + attributes = job_attributes(pipeline, opts) + .merge(commands: '$ build command') + + Ci::Build.create!(attributes).tap do |build| + # We need to set build trace and artifacts after saving a build + # (id required), that is why we need `#tap` method instead of passing + # block directly to `Ci::Build#create!`. + + setup_artifacts(build) + setup_build_log(build) + build.save end end - def build_create!(pipeline, opts = {}) - attributes = build_attributes_for(pipeline, opts) + def setup_artifacts(build) + return unless %w[build test].include?(build.stage) - Ci::Build.create!(attributes) do |build| - if opts[:name].start_with?('build') - artifacts_cache_file(artifacts_archive_path) do |file| - build.artifacts_file = file - end + artifacts_cache_file(artifacts_archive_path) do |file| + build.artifacts_file = file + end - artifacts_cache_file(artifacts_metadata_path) do |file| - build.artifacts_metadata = file - end - end + artifacts_cache_file(artifacts_metadata_path) do |file| + build.artifacts_metadata = file + end + end - if %w(running success failed).include?(build.status) - # We need to set build trace after saving a build (id required) - build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") - end + def setup_build_log(build) + if %w(running success failed).include?(build.status) + build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") end end def commit_status_create!(pipeline, opts = {}) - attributes = commit_status_attributes_for(pipeline, opts) + attributes = job_attributes(pipeline, opts) + GenericCommitStatus.create!(attributes) end - def commit_status_attributes_for(pipeline, opts) + def job_attributes(pipeline, opts) { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, created_at: Time.now, updated_at: Time.now }.merge(opts) end - def build_attributes_for(pipeline, opts) - commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command') - end - def build_user @project.team.users.sample end @@ -131,8 +146,8 @@ class Gitlab::Seeder::Builds end Gitlab::Seeder.quiet do - Project.all.sample(10).each do |project| - project_builds = Gitlab::Seeder::Builds.new(project) + Project.all.sample(5).each do |project| + project_builds = Gitlab::Seeder::Pipelines.new(project) project_builds.seed! end end diff --git a/db/migrate/20160707104333_add_lock_to_issuables.rb b/db/migrate/20160707104333_add_lock_to_issuables.rb new file mode 100644 index 00000000000..54866d02cbc --- /dev/null +++ b/db/migrate/20160707104333_add_lock_to_issuables.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLockToIssuables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :issues, :lock_version, :integer + add_column :merge_requests, :lock_version, :integer + end + + def down + remove_column :issues, :lock_version + remove_column :merge_requests, :lock_version + end +end diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb new file mode 100644 index 00000000000..c8cbd2718ff --- /dev/null +++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestDiffRemoveUniq < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + if index_exists?(:merge_request_diffs, :merge_request_id) + remove_index :merge_request_diffs, :merge_request_id + end + end + + def down + unless index_exists?(:merge_request_diffs, :merge_request_id) + add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true + end + end +end diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb new file mode 100644 index 00000000000..6d04242dd25 --- /dev/null +++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb @@ -0,0 +1,17 @@ +class MergeRequestDiffAddIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + add_concurrent_index :merge_request_diffs, :merge_request_id + end + + def down + if index_exists?(:merge_request_diffs, :merge_request_id) + remove_index :merge_request_diffs, :merge_request_id + end + end +end diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb new file mode 100644 index 00000000000..8db0fc60c4b --- /dev/null +++ b/db/migrate/20160801163421_add_expires_at_to_member.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddExpiresAtToMember < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :members, :expires_at, :date + end +end diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb new file mode 100644 index 00000000000..915d3d78e40 --- /dev/null +++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb @@ -0,0 +1,10 @@ +class AddKodingToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :koding_enabled, :boolean + add_column :application_settings, :koding_url, :string + end +end diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb new file mode 100644 index 00000000000..0ed538b0df8 --- /dev/null +++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :project_group_links, :expires_at, :date + end +end diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb new file mode 100644 index 00000000000..b6e8bb18e7b --- /dev/null +++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.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 AddIndexToNoteDiscussionId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :notes, :discussion_id + end +end diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb new file mode 100644 index 00000000000..0c68cf01900 --- /dev/null +++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'" + end +end diff --git a/db/migrate/20160823081327_change_merge_error_to_text.rb b/db/migrate/20160823081327_change_merge_error_to_text.rb new file mode 100644 index 00000000000..7920389cd83 --- /dev/null +++ b/db/migrate/20160823081327_change_merge_error_to_text.rb @@ -0,0 +1,10 @@ +class ChangeMergeErrorToText < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'This migration requires downtime because it alters a column from varchar(255) to text.' + + def change + change_column :merge_requests, :merge_error, :text, limit: 65535 + end +end diff --git a/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb new file mode 100644 index 00000000000..c169084e976 --- /dev/null +++ b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLfsEnabledToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :projects, :lfs_enabled, :boolean + end +end diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb new file mode 100644 index 00000000000..65cf46308d9 --- /dev/null +++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb @@ -0,0 +1,11 @@ +class DropUnusedCiTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + drop_table(:ci_services) + drop_table(:ci_web_hooks) + end +end diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb new file mode 100644 index 00000000000..7c55bc23cf2 --- /dev/null +++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb @@ -0,0 +1,16 @@ +class EnsureLockVersionHasNoDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_default :issues, :lock_version, nil + change_column_default :merge_requests, :lock_version, nil + + execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0') + execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0') + end + + def down + end +end diff --git a/db/migrate/20160830232601_change_lock_version_not_null.rb b/db/migrate/20160830232601_change_lock_version_not_null.rb new file mode 100644 index 00000000000..01c58ed5bdc --- /dev/null +++ b/db/migrate/20160830232601_change_lock_version_not_null.rb @@ -0,0 +1,13 @@ +class ChangeLockVersionNotNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_null :issues, :lock_version, true + change_column_null :merge_requests, :lock_version, true + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 82d4590f6b5..963d528d170 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: 20160817154936) do +ActiveRecord::Schema.define(version: 20160830232601) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,12 +84,14 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "health_check_access_token" t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 - t.boolean "user_default_external", default: false, null: false t.text "after_sign_up_text" + t.boolean "user_default_external", default: false, null: false t.string "repository_storage", default: "default" t.string "enabled_git_access_protocol" t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" + t.boolean "koding_enabled" + t.string "koding_url" end create_table "audit_events", force: :cascade do |t| @@ -173,8 +175,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.text "artifacts_metadata" t.integer "erased_by_id" t.datetime "erased_at" - t.string "environment" t.datetime "artifacts_expire_at" + t.string "environment" t.integer "artifacts_size" t.string "when" t.text "yaml_variables" @@ -293,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree - create_table "ci_services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false - t.text "properties" - end - create_table "ci_sessions", force: :cascade do |t| t.string "session_id", null: false t.text "data" @@ -358,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree - create_table "ci_web_hooks", force: :cascade do |t| - t.string "url", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -469,6 +454,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.datetime "deleted_at" t.date "due_date" t.integer "moved_to_id" + t.integer "lock_version" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -568,6 +554,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "invite_token" t.datetime "invite_accepted_at" t.datetime "requested_at" + t.date "expires_at" end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree @@ -589,7 +576,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "start_commit_sha" end - add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree + add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree create_table "merge_requests", force: :cascade do |t| t.string "target_branch", null: false @@ -609,13 +596,14 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.integer "position", default: 0 t.datetime "locked_at" t.integer "updated_by_id" - t.string "merge_error" + t.text "merge_error" + t.text "merge_params" t.boolean "merge_when_build_succeeds", default: false, null: false t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" t.string "in_progress_merge_commit_sha" - t.text "merge_params" + t.integer "lock_version" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -699,6 +687,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree + add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -768,10 +757,10 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.integer "user_id", null: false t.string "token", null: false t.string "name", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.boolean "revoked", default: false t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree @@ -783,6 +772,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.datetime "created_at" t.datetime "updated_at" t.integer "group_access", default: 30, null: false + t.date "expires_at" end create_table "project_import_data", force: :cascade do |t| @@ -833,8 +823,9 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false t.boolean "has_external_issue_tracker" t.string "repository_storage", default: "default", null: false - t.boolean "has_external_wiki" t.boolean "request_access_enabled", default: true, null: false + t.boolean "has_external_wiki" + t.boolean "lfs_enabled" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -955,8 +946,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do t.string "noteable_type" t.string "title" t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "submitted_as_ham", default: false, null: false end @@ -1027,13 +1018,13 @@ ActiveRecord::Schema.define(version: 20160817154936) do add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree create_table "user_agent_details", force: :cascade do |t| - t.string "user_agent", null: false - t.string "ip_address", null: false - t.integer "subject_id", null: false - t.string "subject_type", null: false + t.string "user_agent", null: false + t.string "ip_address", null: false + t.integer "subject_id", null: false + t.string "subject_type", null: false t.boolean "submitted", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| diff --git a/doc/README.md b/doc/README.md index fc51ea911b9..254394eb63e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,6 +2,7 @@ ## User documentation +- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc. - [API](api/README.md) Automate GitLab via a simple and powerful API. - [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. @@ -29,6 +30,7 @@ - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md new file mode 100644 index 00000000000..a2c358af095 --- /dev/null +++ b/doc/administration/integration/koding.md @@ -0,0 +1,242 @@ +# Koding & GitLab + +> [Introduced][ce-5909] in GitLab 8.11. + +This document will guide you through installing and configuring Koding with +GitLab. + +First of all, to be able to use Koding and GitLab together you will need public +access to your server. This allows you to use single sign-on from GitLab to +Koding and using vms from cloud providers like AWS. Koding has a registry for +VMs, called Kontrol and it runs on the same server as Koding itself, VMs from +cloud providers register themselves to Kontrol via the agent that we put into +provisioned VMs. This agent is called Klient and it provides Koding to access +and manage the target machine. + +Kontrol and Klient are based on another technology called +[Kite](https://github.com/koding/kite), that we have written at Koding. Which is a +microservice framework that allows you to develop microservices easily. + +## Requirements + +### Hardware + +Minimum requirements are; + + - 2 cores CPU + - 3G RAM + - 10G Storage + +If you plan to use AWS to install Koding it is recommended that you use at +least a `c3.xlarge` instance. + +### Software + + - [Git](https://git-scm.com) + - [Docker](https://www.docker.com) + - [docker-compose](https://www.docker.com/products/docker-compose) + +Koding can run on most of the UNIX based operating systems, since it's shipped +as containerized with Docker support, it can work on any operating system that +supports Docker. + +Required services are: + +- **PostgreSQL** - Kontrol and Service DB provider +- **MongoDB** - Main DB provider the application +- **Redis** - In memory DB used by both application and services +- **RabbitMQ** - Message Queue for both application and services + +which are also provided as a Docker container by Koding. + + +## Getting Started with Development Versions + + +### Koding + +You can run `docker-compose` environment for developing koding by +executing commands in the following snippet. + +```bash +git clone https://github.com/koding/koding.git +cd koding +docker-compose up +``` + +This should start koding on `localhost:8090`. + +By default there is no team exists in Koding DB. You'll need to create a team +called `gitlab` which is the default team name for GitLab integration in the +configuration. To make things in order it's recommended to create the `gitlab` +team first thing after setting up Koding. + + +### GitLab + +To install GitLab to your environment for development purposes it's recommended +to use GitLab Development Kit which you can get it from +[here](https://gitlab.com/gitlab-org/gitlab-development-kit). + +After all those steps, gitlab should be running on `localhost:3000` + + +## Integration + +Integration includes following components; + + - Single Sign On with OAuth from GitLab to Koding + - System Hook integration for handling GitLab events on Koding + (`project_created`, `user_joined` etc.) + - Service endpoints for importing/executing stacks from GitLab to Koding + (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs) + +As it's pointed out before, you will need public access to this machine that +you've installed Koding and GitLab on. Better to use a domain but a static IP +is also fine. + +For IP based installation you can use [xip.io](https://xip.io) service which is +free and provides DNS resolution to IP based requests like following; + + - 127.0.0.1.xip.io -> resolves to 127.0.0.1 + - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1 + - and so on... + +As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for +a running koding instance on `127.0.0.1` server will be handled as `foo` team +requests. + + +### GitLab Side + +You need to enable Koding integration from Settings under Admin Area. To do +that login with an Admin account and do followings; + + - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings) + - scroll to bottom of the page until Koding section + - check `Enable Koding` checkbox + - provide GitLab team page for running Koding instance as `Koding URL`* + +* For `Koding URL` you need to provide the gitlab integration enabled team on +your Koding installation. Team called `gitlab` has integration on Koding out +of the box, so if you didn't change anything your team on Koding should be +`gitlab`. + +So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs +to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host +with your Koding installation here. + + +#### Registering Koding for OAuth integration + +We need `Application ID` and `Secret` to enable login to Koding via GitLab +feature and to do that you need to register running Koding as a new application +to your running GitLab application. Follow +[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to +enable this integration. + +Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback` +which again you need to _replace `127.0.0.1` with your instance public IP._ + +Take a copy of `Application ID` and `Secret` that is generated by the GitLab +application, we will need those on _Koding Part_ of this guide. + + +#### Registering system hooks to Koding (optional) + +Koding can take actions based on the events generated by GitLab application. +This feature is still in progress and only following events are processed by +Koding at the moment; + + - user_create + - user_destroy + +All system events are handled but not implemented on Koding side. + +To enable this feature you need to provide a `URL` and a `Secret Token` to your +GitLab application. Open your admin area on your GitLab app from +[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks) +and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the +endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and +keep a copy of it, we will need it on _Koding Part_ of this guide. + +_(replace `127.0.0.1` with your instance public IP)_ + + +### Koding Part + +If you followed the steps in GitLab part we should have followings to enable +Koding part integrations; + + - `Application ID` and `Secret` for OAuth integration + - `Secret Token` for system hook integration + - Public address of running GitLab instance + + +#### Start Koding with GitLab URL + +Now we need to configure Koding with all this information to get things ready. +If it's already running please stop koding first. + +##### From command-line + +Replace followings with the ones you got from GitLab part of this guide; + +```bash +cd koding +docker-compose run \ + --service-ports backend \ + /opt/koding/scripts/bootstrap-container build \ + --host=**YOUR_IP**.xip.io \ + --gitlabHost=**GITLAB_IP** \ + --gitlabPort=**GITLAB_PORT** \ + --gitlabToken=**SECRET_TOKEN** \ + --gitlabAppId=**APPLICATION_ID** \ + --gitlabAppSecret=**SECRET** +``` + +##### By updating configuration + +Alternatively you can update `gitlab` section on +`config/credentials.default.coffee` like following; + +``` +gitlab = + host: '**GITLAB_IP**' + port: '**GITLAB_PORT**' + applicationId: '**APPLICATION_ID**' + applicationSecret: '**SECRET**' + team: 'gitlab' + redirectUri: '' + systemHookToken: '**SECRET_TOKEN**' + hooksEnabled: yes +``` + +and start by only providing the `host`; + +```bash +cd koding +docker-compose run \ + --service-ports backend \ + /opt/koding/scripts/bootstrap-container build \ + --host=**YOUR_IP**.xip.io \ +``` + +#### Enable Single Sign On + +Once you restarted your Koding and logged in with your username and password +you need to activate oauth authentication for your user. To do that + + - Navigate to Dashboard on Koding from; + `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account` + - Scroll down to Integrations section + - Click on toggle to turn On integration in GitLab integration section + +This will redirect you to your GitLab instance and will ask your permission ( +if you are not logged in to GitLab at this point you will be redirected after +login) once you accept you will be redirected to your Koding instance. + +From now on you can login by using `SIGN IN WITH GITLAB` button on your Login +screen in your Koding instance. + +[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909 diff --git a/doc/api/commits.md b/doc/api/commits.md index 5c98c5d7565..55d0de7afd9 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -10,7 +10,7 @@ GET /projects/:id/repository/commits | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | | `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | @@ -58,7 +58,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -102,7 +102,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -138,7 +138,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | ```bash @@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA or name of a repository branch or tag | | `note` | string | yes | The text of the comment | | `path` | string | no | The file path relative to the repository | @@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA | `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch | `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` @@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project +| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA | `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled` | `ref` | string | no | The `ref` (branch or tag) to which the status refers diff --git a/doc/api/issues.md b/doc/api/issues.md index a665645ad0e..b194799ccbf 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -79,7 +79,8 @@ Example response: "labels" : [], "subscribed" : false, "user_notes_count": 1, - "due_date": "2016-07-22" + "due_date": "2016-07-22", + "web_url": "http://example.com/example/example/issues/6" } ] ``` @@ -156,7 +157,8 @@ Example response: "created_at" : "2016-01-04T15:31:46.176Z", "subscribed" : false, "user_notes_count": 1, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/1" } ] ``` @@ -235,7 +237,8 @@ Example response: "created_at" : "2016-01-04T15:31:46.176Z", "subscribed" : false, "user_notes_count": 1, - "due_date": "2016-07-22" + "due_date": "2016-07-22", + "web_url": "http://example.com/example/example/issues/1" } ] ``` @@ -299,7 +302,8 @@ Example response: "created_at" : "2016-01-04T15:31:46.176Z", "subscribed": false, "user_notes_count": 1, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/1" } ``` @@ -323,8 +327,8 @@ POST /projects/:id/issues | `assignee_id` | integer | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | -| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | -| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | +| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug @@ -357,7 +361,8 @@ Example response: "milestone" : null, "subscribed" : true, "user_notes_count": 0, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/14" } ``` @@ -384,8 +389,8 @@ PUT /projects/:id/issues/:issue_id | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | -| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | -| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | +| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close @@ -418,7 +423,8 @@ Example response: "milestone" : null, "subscribed" : true, "user_notes_count": 0, - "due_date": "2016-07-22" + "due_date": "2016-07-22", + "web_url": "http://example.com/example/example/issues/15" } ``` @@ -496,7 +502,8 @@ Example response: "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "web_url": "https://gitlab.example.com/u/solon.cremin" }, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/11" } ``` @@ -551,7 +558,8 @@ Example response: "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", "web_url": "https://gitlab.example.com/u/solon.cremin" }, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/11" } ``` @@ -607,7 +615,8 @@ Example response: "web_url": "https://gitlab.example.com/u/orville" }, "subscribed": false, - "due_date": null + "due_date": null, + "web_url": "http://example.com/example/example/issues/12" } ``` @@ -693,7 +702,9 @@ Example response: "subscribed": true, "user_notes_count": 7, "upvotes": 0, - "downvotes": 0 + "downvotes": 0, + "due_date": null, + "web_url": "http://example.com/example/example/issues/110" }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10", "body": "Vel voluptas atque dicta mollitia adipisci qui at.", diff --git a/doc/api/members.md b/doc/api/members.md index d002e6eaf89..fd6d728dad2 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -86,7 +86,8 @@ Example response: "name": "Raymond Smith", "state": "active", "created_at": "2012-10-22T14:13:35Z", - "access_level": 30 + "access_level": 30, + "expires_at": null } ``` @@ -106,6 +107,7 @@ POST /projects/:id/members | `id` | integer/string | yes | The group/project ID or path | | `user_id` | integer | yes | The user ID of the new member | | `access_level` | integer | yes | A valid access level | +| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30 @@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id | `id` | integer/string | yes | The group/project ID or path | | `user_id` | integer | yes | The user ID of the member | | `access_level` | integer | yes | A valid access level | +| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40 diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 3e88a758936..f4760ceac7c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -70,7 +70,8 @@ Parameters: "subscribed" : false, "user_notes_count": 1, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ] ``` @@ -136,7 +137,8 @@ Parameters: "subscribed" : true, "user_notes_count": 1, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ``` @@ -239,6 +241,7 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1", "changes": [ { "old_path": "VERSION", @@ -321,7 +324,8 @@ Parameters: "subscribed" : true, "user_notes_count": 0, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ``` @@ -395,7 +399,8 @@ Parameters: "subscribed" : true, "user_notes_count": 1, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ``` @@ -496,7 +501,8 @@ Parameters: "subscribed" : true, "user_notes_count": 1, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ``` @@ -565,7 +571,8 @@ Parameters: "subscribed" : true, "user_notes_count": 1, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" } ``` @@ -886,7 +893,8 @@ Example response: "subscribed": true, "user_notes_count": 7, "should_remove_source_branch": true, - "force_remove_source_branch": false + "force_remove_source_branch": false, + "web_url": "http://example.com/example/example/merge_requests/1" }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7", "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.", @@ -894,3 +902,112 @@ Example response: "created_at": "2016-07-01T11:14:15.530Z" } ``` + +## Get MR diff versions + +Get a list of merge request diff versions. + +``` +GET /projects/:id/merge_requests/:merge_request_id/versions +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | String | yes | The ID of the project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions +``` + +Example response: + +```json +[{ + "id": 110, + "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30", + "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "created_at": "2016-07-26T14:44:48.926Z", + "merge_request_id": 105, + "state": "collected", + "real_size": "1" +}, { + "id": 108, + "head_commit_sha": "3eed087b29835c48015768f839d76e5ea8f07a24", + "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "created_at": "2016-07-25T14:21:33.028Z", + "merge_request_id": 105, + "state": "collected", + "real_size": "1" +}] +``` + +## Get a single MR diff version + +Get a single merge request diff version. + +``` +GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | String | yes | The ID of the project | +| `merge_request_id` | integer | yes | The ID of the merge request | +| `version_id` | integer | yes | The ID of the merge request diff version | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1 +``` + +Example response: + +```json +{ + "id": 110, + "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30", + "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd", + "created_at": "2016-07-26T14:44:48.926Z", + "merge_request_id": 105, + "state": "collected", + "real_size": "1", + "commits": [{ + "id": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30", + "short_id": "33e2ee85", + "title": "Change year to 2018", + "author_name": "Administrator", + "author_email": "admin@example.com", + "created_at": "2016-07-26T17:44:29.000+03:00", + "message": "Change year to 2018" + }, { + "id": "aa24655de48b36335556ac8a3cd8bb521f977cbd", + "short_id": "aa24655d", + "title": "Update LICENSE", + "author_name": "Administrator", + "author_email": "admin@example.com", + "created_at": "2016-07-25T17:21:53.000+03:00", + "message": "Update LICENSE" + }, { + "id": "3eed087b29835c48015768f839d76e5ea8f07a24", + "short_id": "3eed087b", + "title": "Add license", + "author_name": "Administrator", + "author_email": "admin@example.com", + "created_at": "2016-07-25T17:21:20.000+03:00", + "message": "Add license" + }], + "diffs": [{ + "old_path": "LICENSE", + "new_path": "LICENSE", + "a_mode": "0", + "b_mode": "100644", + "diff": "--- /dev/null\n+++ b/LICENSE\n@@ -0,0 +1,21 @@\n+The MIT License (MIT)\n+\n+Copyright (c) 2018 Administrator\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }] +} +``` diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index a7acf37b5bc..c6685f54a9d 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -53,7 +53,8 @@ Parameters: }, "expires_at": null, "updated_at": "2012-06-28T10:52:04Z", - "created_at": "2012-06-28T10:52:04Z" + "created_at": "2012-06-28T10:52:04Z", + "web_url": "http://example.com/example/example/snippets/1" } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 37d97b2db44..0d5aa61aa74 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -84,7 +84,8 @@ Parameters: "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false }, { "id": 6, @@ -144,7 +145,8 @@ Parameters: "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false } ] ``` @@ -280,7 +282,8 @@ Parameters: "group_name": "Gitlab Org", "group_access_level": 10 } - ] + ], + "only_allow_merge_if_build_succeeds": false } ``` @@ -448,6 +451,8 @@ Parameters: - `visibility_level` (optional) - `import_url` (optional) - `public_builds` (optional) +- `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Create project for user @@ -473,6 +478,8 @@ Parameters: - `visibility_level` (optional) - `import_url` (optional) - `public_builds` (optional) +- `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Edit project @@ -484,7 +491,7 @@ PUT /projects/:id Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `name` (optional) - project name - `path` (optional) - repository name for project - `description` (optional) - short project description @@ -499,6 +506,8 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) +- `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. @@ -513,7 +522,7 @@ POST /projects/fork/:id Parameters: -- `id` (required) - The ID of the project to be forked +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ### Star a project @@ -526,7 +535,7 @@ POST /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" @@ -577,7 +586,8 @@ Example response: "forks_count": 0, "star_count": 1, "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false } ``` @@ -592,7 +602,7 @@ DELETE /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" @@ -643,7 +653,8 @@ Example response: "forks_count": 0, "star_count": 0, "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false } ``` @@ -662,7 +673,7 @@ POST /projects/:id/archive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" @@ -729,7 +740,8 @@ Example response: "star_count": 0, "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false } ``` @@ -748,7 +760,7 @@ POST /projects/:id/unarchive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" @@ -815,7 +827,8 @@ Example response: "star_count": 0, "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "public_builds": true, - "shared_with_groups": [] + "shared_with_groups": [], + "only_allow_merge_if_build_succeeds": false } ``` @@ -829,7 +842,7 @@ DELETE /projects/:id Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ## Uploads @@ -843,7 +856,7 @@ POST /projects/:id/uploads Parameters: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `file` (required) - The file to be uploaded ```json @@ -872,7 +885,7 @@ POST /projects/:id/share Parameters: -- `id` (required) - The ID of a project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `group_id` (required) - The ID of a group - `group_access` (required) - Level of permissions for sharing @@ -914,7 +927,11 @@ Parameters: "push_events": true, "issues_events": true, "merge_requests_events": true, + "tag_push_events": true, "note_events": true, + "build_events": true, + "pipeline_events": true, + "wiki_page_events": true, "enable_ssl_verification": true, "created_at": "2012-10-12T17:04:47Z" } @@ -937,6 +954,9 @@ Parameters: - `merge_requests_events` - Trigger hook on merge_requests events - `tag_push_events` - Trigger hook on push_tag events - `note_events` - Trigger hook on note events +- `build_events` - Trigger hook on build events +- `pipeline_events` - Trigger hook on pipeline events +- `wiki_page_events` - Trigger hook on wiki page events - `enable_ssl_verification` - Do SSL verification when triggering the hook ### Edit project hook @@ -957,6 +977,9 @@ Parameters: - `merge_requests_events` - Trigger hook on merge_requests events - `tag_push_events` - Trigger hook on push_tag events - `note_events` - Trigger hook on note events +- `build_events` - Trigger hook on build events +- `pipeline_events` - Trigger hook on pipeline events +- `wiki_page_events` - Trigger hook on wiki page events - `enable_ssl_verification` - Do SSL verification when triggering the hook ### Delete project hook @@ -978,6 +1001,8 @@ is available before it is returned in the JSON response or an empty response is ## Branches +For more information please consult the [Branches](branches.md) documentation. + ### List branches Lists all branches of a project. @@ -996,56 +1021,46 @@ Parameters: "name": "async", "commit": { "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca", - "parents": [ - { - "id": "3f94fc7c85061973edc9906ae170cc269b07ca55" - } + "parent_ids": [ + "3f94fc7c85061973edc9906ae170cc269b07ca55" ], - "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee", "message": "give Caolan credit where it's due (up top)", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2010-12-08T21:28:50+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2010-12-08T21:28:50+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false }, { "name": "gh-pages", "commit": { "id": "101c10a60019fe870d21868835f65c25d64968fc", - "parents": [ - { - "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa" - } + "parent_ids": [ + "9c15d2e26945a665131af5d7b6d30a06ba338aaa" ], - "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a", "message": "Underscore.js 1.5.2", - "author": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, - "committer": { - "name": "Jeremy Ashkenas", - "email": "jashkenas@example.com" - }, + "author_name": "Jeremy Ashkenas", + "author_email": "jashkenas@example.com", "authored_date": "2013-09-07T12:58:21+00:00", + "committer_name": "Jeremy Ashkenas", + "committer_email": "jashkenas@example.com", "committed_date": "2013-09-07T12:58:21+00:00" }, - "protected": false + "protected": false, + "developers_can_push": false, + "developers_can_merge": false } ] ``` -### List single branch +### Single branch -Lists a specific branch of a project. +A specific branch of a project. ``` GET /projects/:id/repository/branches/:branch @@ -1055,6 +1070,8 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `branch` (required) - The name of the branch. +- `developers_can_push` - Flag if developers can push to the branch. +- `developers_can_merge` - Flag if developers can merge to the branch. ### Protect single branch @@ -1094,7 +1111,7 @@ POST /projects/:id/fork/:forked_from_id Parameters: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `forked_from_id:` (required) - The ID of the project that was forked from ### Delete an existing forked from relationship @@ -1105,7 +1122,7 @@ DELETE /projects/:id/fork Parameter: -- `id` (required) - The ID of the project +- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked ## Search for projects by name diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index d90d7aca4fd..ca9b986a060 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -5,7 +5,7 @@ Introduced in GitLab 8.8. ## Pipelines -A pipeline is a group of [builds] that get executed in [stages] (batches). All +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) @@ -67,6 +67,8 @@ use following Markdown code to embed the est coverage report into `README.md`: ![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. + [builds]: #builds [jobs]: yaml/README.md#jobs [stages]: yaml/README.md#stages diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e7850aa2c9d..58d5306f12a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our - [before_script and after_script](#before_script-and-after_script) - [Git Strategy](#git-strategy) - [Shallow cloning](#shallow-cloning) -- [Hidden jobs](#hidden-jobs) +- [Hidden keys](#hidden-keys) - [Special YAML features](#special-yaml-features) - [Anchors](#anchors) - [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) @@ -934,24 +934,27 @@ variables: GIT_DEPTH: "3" ``` -## Hidden jobs +## Hidden keys >**Note:** Introduced in GitLab 8.6 and GitLab Runner v1.1.1. -Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can +Keys that start with a dot (`.`) will be not processed by GitLab CI. You can use this feature to ignore jobs, or use the -[special YAML features](#special-yaml-features) and transform the hidden jobs +[special YAML features](#special-yaml-features) and transform the hidden keys into templates. -In the following example, `.job_name` will be ignored: +In the following example, `.key_name` will be ignored: ```yaml -.job_name: +.key_name: script: - rake spec ``` +Hidden keys can be hashes like normal CI jobs, but you are also allowed to use +different types of structures to leverage special YAML features. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) @@ -967,7 +970,7 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1. YAML also has a handy feature called 'anchors', which let you easily duplicate content across your document. Anchors can be used to duplicate/inherit -properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs) +properties, and is a perfect example to be used with [hidden keys](#hidden-keys) to provide templates for your jobs. The following example uses anchors and map merging. It will create two jobs, @@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs, having their own custom `script` defined: ```yaml -.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition' +.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition' image: ruby:2.1 services: - postgres @@ -1081,7 +1084,7 @@ test:mysql: - ruby ``` -You can see that the hidden jobs are conveniently used as templates. +You can see that the hidden keys are conveniently used as templates. ## Validate the .gitlab-ci.yml diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 927a1872413..37bb59e112c 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -6,7 +6,7 @@ it organized and easy to find. ## Location and naming of documents >**Note:** -These guidelines derive from the discussion taken place in issue [#3349](ce-3349). +These guidelines derive from the discussion taken place in issue [#3349][ce-3349]. The documentation hierarchy can be vastly improved by providing a better layout and organization of directories. @@ -222,18 +222,26 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to ``` 1. Find and replace any occurrences of the old location with the new one. - A quick way to find them is to use `grep`: + A quick way to find them is to use `git grep`. First go to the root directory + where you cloned the `gitlab-ce` repository and then do: ``` - grep -nR "lfs_administration.md" doc/ + git grep -n "workflow/lfs/lfs_administration" + git grep -n "lfs/lfs_administration" ``` - The above command will search in the `doc/` directory for - `lfs_administration.md` recursively and will print the file and the line - where this file is mentioned. Note that we used just the filename - (`lfs_administration.md`) and not the whole the relative path - (`workflow/lfs/lfs_administration.md`). +Things to note: +- Since we also use inline documentation, except for the documentation itself, + the document might also be referenced in the views of GitLab (`app/`) which will + render when visiting `/help`, and sometimes in the testing suite (`spec/`). +- The above `git grep` command will search recursively in the directory you run + it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration` + and will print the file and the line where this file is mentioned. + You may ask why the two greps. Since we use relative paths to link to + documentation, sometimes it might be useful to search a path deeper. +- The `*.md` extension is not used when a document is linked to GitLab's + built-in help page, that's why we omit it in `git grep`. ## Configuration documentation for source and Omnibus installations @@ -422,7 +430,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation" [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 diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md index e03adcaadea..32aac2529a4 100644 --- a/doc/development/newlines_styleguide.md +++ b/doc/development/newlines_styleguide.md @@ -2,7 +2,7 @@ This style guide recommends best practices for newlines in Ruby code. -## Rule: separate code with newlines only when it makes sense from logic perspectice +## Rule: separate code with newlines only to group together related logic ```ruby # bad diff --git a/doc/install/installation.md b/doc/install/installation.md index eb9606934cd..d4b89fa8345 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -397,7 +397,7 @@ If you are not using Linux you may have to run `gmake` instead of cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.8 + sudo -u git -H git checkout v0.7.11 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/install/requirements.md b/doc/install/requirements.md index a65ac8a5f79..571f1a38358 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 or 2.3. +GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13. You will have to use the standard MRI implementation of Ruby. We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab diff --git a/doc/integration/README.md b/doc/integration/README.md index ddbd570ac6c..c2fd299db07 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services. - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam +- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 2eb6266ebe7..556d71b8b76 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,111 +1,164 @@ -# Integrate your server with Bitbucket +# Integrate your GitLab server with Bitbucket -Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. +Import projects from Bitbucket.org and login to your GitLab instance with your +Bitbucket.org account. -To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. -Bitbucket will generate an application ID and secret key for you to use. +## Overview -1. Sign in to Bitbucket. +You can set up Bitbucket.org as an OAuth provider so that you can use your +credentials to authenticate into GitLab or import your projects from +Bitbucket.org. -1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. +- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth + provider](#bitbucket-omniauth-provider) section. +- To import projects from Bitbucket, follow both the + [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and + [Bitbucket project import](#bitbucket-project-import) sections. -1. Select "OAuth" in the left menu. +## Bitbucket OmniAuth provider -1. Select "Add consumer". +> **Note:** +Make sure to first follow the [Initial OmniAuth configuration][init-oauth] +before proceeding with setting up the Bitbucket integration. -1. Provide the required details. - - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - - Application description: Fill this in if you wish. - - URL: The URL to your GitLab installation. 'https://gitlab.company.com' -1. Select "Save". +To enable the Bitbucket OmniAuth provider you must register your application +with Bitbucket.org. Bitbucket will generate an application ID and secret key for +you to use. -1. You should now see a Key and Secret in the list of OAuth customers. - Keep this page open as you continue configuration. +1. Sign in to [Bitbucket.org](https://bitbucket.org). +1. Navigate to your individual user settings (**Bitbucket settings**) or a team's + settings (**Manage team**), depending on how you want the application registered. + It does not matter if the application is registered as an individual or a + team, that is entirely up to you. +1. Select **OAuth** in the left menu under "Access Management". +1. Select **Add consumer**. +1. Provide the required details: -1. On your GitLab server, open the configuration file. + | Item | Description | + | :--- | :---------- | + | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. | + | **Application description** | Fill this in if you wish. | + | **Callback URL** | Leave blank. | + | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | - For omnibus package: + And grant at least the following permissions: - ```sh - sudo editor /etc/gitlab/gitlab.rb + ``` + Account: Email + Repositories: Read, Admin ``` - For installations from source: + >**Note:** + It may seem a little odd to giving GitLab admin permissions to repositories, + but this is needed in order for GitLab to be able to clone the repositories. - ```sh - cd /home/git/gitlab + ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png) + +1. Select **Save**. +1. Select your newly created OAuth consumer and you should now see a Key and + Secret in the list of OAuth customers. Keep this page open as you continue + the configuration. + + ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png) + +1. On your GitLab server, open the configuration file: - sudo -u git -H editor config/gitlab.yml ``` + # For Omnibus packages + sudo editor /etc/gitlab/gitlab.rb -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + # For installations from source + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` -1. Add the provider configuration: +1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) + for initial settings. +1. Add the Bitbucket provider configuration: - For omnibus package: + For Omnibus packages: ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "bitbucket", - "app_id" => "YOUR_KEY", - "app_secret" => "YOUR_APP_SECRET", - "url" => "https://bitbucket.org/" - } - ] + gitlab_rails['omniauth_providers'] = [ + { + "name" => "bitbucket", + "app_id" => "BITBUCKET_APP_KEY", + "app_secret" => "BITBUCKET_APP_SECRET", + "url" => "https://bitbucket.org/" + } + ] ``` - For installation from source: + For installations from source: - ``` - - { name: 'bitbucket', app_id: 'YOUR_KEY', - app_secret: 'YOUR_APP_SECRET' } + ```yaml + - { name: 'bitbucket', + app_id: 'BITBUCKET_APP_KEY', + app_secret: 'BITBUCKET_APP_SECRET' } ``` -1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7. + --- -1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. + Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret + from the Bitbucket application page. 1. Save the configuration file. +1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. -1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). - -1. Restart GitLab for the changes to take effect. - -On the sign in page there should now be a Bitbucket icon below the regular sign in form. -Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Bitbucket icon below the regular sign +in form. Click the icon to begin the authentication process. Bitbucket will ask +the user to sign in and authorize the GitLab application. If everything goes +well, the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. +To allow projects to be imported directly into GitLab, Bitbucket requires two +extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md). -Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. +Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and +instead requires GitLab to use SSH and identify itself using your GitLab +server's SSH key. -### Step 1: Public key +To be able to access repositories on Bitbucket, GitLab will automatically +register your public key with Bitbucket as a deploy key for the repositories to +be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which +translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to +`/home/git/.ssh/bitbucket_rsa.pub` for installations from source. -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. +--- -If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: +Below are the steps that will allow GitLab to be able to import your projects +from Bitbucket. -1. Create a new SSH key: +1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider). +1. Create a new SSH key with an **empty passphrase**: ```sh sudo -u git -H ssh-keygen ``` - When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. - Make sure to use an **empty passphrase**. + When asked to 'Enter file in which to save the key' enter: + `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or + `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is + important so make sure to get it right. -1. Configure SSH client to use your new key: + > **Warning:** + This key must NOT be associated with ANY existing Bitbucket accounts. If it + is, the import will fail with an `Access denied! Please verify you can add + deploy keys to this repository.` error. - Open the SSH configuration file of the git user. +1. Next, you need to to configure the SSH client to use your new key. Open the + SSH configuration file of the `git` user: - ```sh - sudo editor /home/git/.ssh/config + ``` + # For Omnibus packages + sudo editor /var/opt/gitlab/.ssh/config + + # For installations from source + sudo editor /home/git/.ssh/config ``` - Add a host configuration for `bitbucket.org`. +1. Add a host configuration for `bitbucket.org`: ```sh Host bitbucket.org @@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec User git ``` -### Step 2: Known hosts - -To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: - -1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: +1. Save the file and exit. +1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git` + user that GitLab will use: ```sh sudo -u git -H ssh bitbucket.org ``` -1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + That step is performed because GitLab needs to connect to Bitbucket over SSH, + in order to add `bitbucket.org` to your GitLab server's known SSH hosts. + +1. Verify the RSA key fingerprint you'll see in the response matches the one + in the [Bitbucket documentation][bitbucket-docs] (the specific IP address + doesn't matter): ```sh - The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. - RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established. + RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A. Are you sure you want to continue connecting (yes/no)? ``` -1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. +1. If the fingerprint matches, type `yes` to continue connecting and have + `bitbucket.org` be added to your known SSH hosts. After confirming you should + see a permission denied message. If you see an authentication successful + message you have done something wrong. The key you are using has already been + added to a Bitbucket account and will cause the import script to fail. Ensure + the key you are using CANNOT authenticate with Bitbucket. +1. Restart GitLab to allow it to find the new public key. -1. Your GitLab server is now able to connect to Bitbucket over SSH. +Your GitLab server is now able to connect to Bitbucket over SSH. You should be +able to see the "Import projects from Bitbucket" option on the New Project page +enabled. -1. Restart GitLab to allow it to find the new public key. +## Acknowledgemts + +Special thanks to the writer behind the following article: + +- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/ -You should now see the "Import projects from Bitbucket" option on the New Project page enabled. +[init-oauth]: omniauth.md#initial-omniauth-configuration +[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png Binary files differnew file mode 100644 index 00000000000..3fb2f7524a3 --- /dev/null +++ b/doc/integration/img/bitbucket_oauth_keys.png diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png Binary files differnew file mode 100644 index 00000000000..a3047712d8c --- /dev/null +++ b/doc/integration/img/bitbucket_oauth_settings_page.png diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 46b260e7033..8a55fce96fe 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -102,8 +102,8 @@ To change these settings: block_auto_created_users: true ``` -Now we can choose one or more of the Supported Providers listed above to continue -the configuration process. +Now we can choose one or more of the [Supported Providers](#supported-providers) +listed above to continue the configuration process. ## Enable OmniAuth for an Existing User diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 84c624cbcb7..b058f8e2a03 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -20,7 +20,31 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` -### 3. Get latest code +### 3. Update Ruby + +If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz +echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz +cd ruby-2.3.1 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Get latest code ```bash sudo -u git -H git fetch --all @@ -41,15 +65,15 @@ For GitLab Enterprise Edition: sudo -u git -H git checkout 8-11-stable-ee ``` -### 4. Update gitlab-shell +### 5. Update gitlab-shell ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.3.3 +sudo -u git -H git checkout v3.4.0 ``` -### 5. Update gitlab-workhorse +### 6. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) which should already be on your system from @@ -58,11 +82,11 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.7.8 +sudo -u git -H git checkout v0.7.11 sudo -u git -H make ``` -### 6. Install libs, migrations, etc. +### 7. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -84,7 +108,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ``` -### 7. Update configuration files +### 8. Update configuration files #### New configuration options for `gitlab.yml` @@ -133,12 +157,12 @@ Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -### 8. Start application +### 9. Start application sudo service gitlab start sudo service nginx restart -### 9. Check application status +### 10. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/account/security.md b/doc/user/account/security.md new file mode 100644 index 00000000000..816094bf8d2 --- /dev/null +++ b/doc/user/account/security.md @@ -0,0 +1,3 @@ +# Account Security + +- [Two-Factor Authentication](two_factor_authentication.md) diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md new file mode 100644 index 00000000000..881358ed94d --- /dev/null +++ b/doc/user/account/two_factor_authentication.md @@ -0,0 +1,68 @@ +# Two-Factor Authentication + +## Recovery options + +If you lose your code generation device (such as your mobile phone) and you need +to disable two-factor authentication on your account, you have several options. + +### Use a saved recovery code + +When you enabled two-factor authentication for your account, a series of +recovery codes were generated. If you saved those codes somewhere safe, you +may use one to sign in. + +First, enter your username/email and password on the GitLab sign in page. When +prompted for a two-factor code, enter one of the recovery codes you saved +previously. + +> **Note:** Once a particular recovery code has been used, it cannot be used again. + You may still use the other saved recovery codes at a later time. + +### Generate new recovery codes using SSH + +It's not uncommon for users to forget to save the recovery codes when enabling +two-factor authentication. If you have an SSH key added to your GitLab account, +you can generate a new set of recovery codes using SSH. + +Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to +confirm that you wish to generate new codes. If you choose to continue, any +previously saved codes will be invalidated. + +```bash +$ ssh git@gitlab.example.com 2fa_recovery_codes +Are you sure you want to generate new two-factor recovery codes? +Any existing recovery codes you saved will be invalidated. (yes/no) +yes + +Your two-factor authentication recovery codes are: + +119135e5a3ebce8e +11f6v2a498810dcd +3924c7ab2089c902 +e79a3398bfe4f224 +34bd7b74adbc8861 +f061691d5107df1a +169bf32a18e63e7f +b510e7422e81c947 +20dbed24c5e74663 +df9d3b9403b9c9f0 + +During sign in, use one of the codes above when prompted for +your two-factor code. Then, visit your Profile Settings and add +a new device so you do not lose access to your account again. +``` + +Next, go to the GitLab sign in page and enter your username/email and password. +When prompted for a two-factor code, enter one of the recovery codes obtained +from the command line output. + +> **Note:** After signing in, you should immediately visit your **Profile Settings + -> Account** to set up two-factor authentication with a new device. + +### Ask a GitLab administrator to disable two-factor on your account + +If the above two methods are not possible, you may ask a GitLab global +administrator to disable two-factor authentication for your account. Please +be aware that this will temporarily leave your account in a less secure state. +You should sign in and re-enable two-factor authentication as soon as possible +after the administrator disables it. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 7fe96e67dbb..c7fda8a497f 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -66,7 +66,7 @@ dependency to do so. Please see the [github-markup gem readme](https://github.co ## Newlines > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). @@ -86,7 +86,7 @@ Sugar is sweet ## Multiple underscores in words > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiple-underscores-in-words +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words: @@ -101,7 +101,7 @@ do_this_and_do_that_and_another_thing ## URL auto-linking > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#url-auto-linking +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking GFM will autolink almost any URL you copy and paste into your text: @@ -122,7 +122,7 @@ GFM will autolink almost any URL you copy and paste into your text: ## Multiline Blockquote > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiline-blockquote +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines, GFM supports multiline blockquotes fenced by <code>>>></code>: @@ -156,7 +156,7 @@ you can quote that without having to manually prepend `>` to every line! ## Code and Syntax Highlighting > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#code-and-syntax-highlighting +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting _GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a list of supported languages visit the Rouge website._ @@ -226,7 +226,7 @@ But let's throw in a <b>tag</b>. ## Inline Diff > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#inline-diff +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff With inline diffs tags you can display {+ additions +} or [- deletions -]. @@ -242,7 +242,7 @@ However the wrapping tags cannot be mixed as such: ## Emoji > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#emoji +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: @@ -307,7 +307,7 @@ GFM also recognizes certain cross-project references: ## Task Lists > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#task-lists +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: @@ -330,7 +330,7 @@ Task lists can only be created in descriptions, not in titles. Task item state c ## Videos > If this is not rendered correctly, see -https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos Image tags with a video extension are automatically converted to a video player. @@ -780,7 +780,7 @@ A link starting with a `/` is relative to the wiki root. - The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. -[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md +[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differnew file mode 100644 index 00000000000..63c269f6dbc --- /dev/null +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png Binary files differnew file mode 100644 index 00000000000..2b8c10eaa0a --- /dev/null +++ b/doc/user/project/img/issue_board_add_list.png diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png Binary files differnew file mode 100644 index 00000000000..112ea171539 --- /dev/null +++ b/doc/user/project/img/issue_board_search_backlog.png diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png Binary files differnew file mode 100644 index 00000000000..b69ef034954 --- /dev/null +++ b/doc/user/project/img/issue_board_system_notes.png diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png Binary files differnew file mode 100644 index 00000000000..b757faeb230 --- /dev/null +++ b/doc/user/project/img/issue_board_welcome_message.png diff --git a/doc/user/project/img/koding_build-in-progress.png b/doc/user/project/img/koding_build-in-progress.png Binary files differnew file mode 100644 index 00000000000..f8cc81834c4 --- /dev/null +++ b/doc/user/project/img/koding_build-in-progress.png diff --git a/doc/user/project/img/koding_build-logs.png b/doc/user/project/img/koding_build-logs.png Binary files differnew file mode 100644 index 00000000000..a04cd5aff99 --- /dev/null +++ b/doc/user/project/img/koding_build-logs.png diff --git a/doc/user/project/img/koding_build-success.png b/doc/user/project/img/koding_build-success.png Binary files differnew file mode 100644 index 00000000000..2a0dd296480 --- /dev/null +++ b/doc/user/project/img/koding_build-success.png diff --git a/doc/user/project/img/koding_commit-koding.yml.png b/doc/user/project/img/koding_commit-koding.yml.png Binary files differnew file mode 100644 index 00000000000..3e133c50327 --- /dev/null +++ b/doc/user/project/img/koding_commit-koding.yml.png diff --git a/doc/user/project/img/koding_different-stack-on-mr-try.png b/doc/user/project/img/koding_different-stack-on-mr-try.png Binary files differnew file mode 100644 index 00000000000..fd25e32f648 --- /dev/null +++ b/doc/user/project/img/koding_different-stack-on-mr-try.png diff --git a/doc/user/project/img/koding_edit-on-ide.png b/doc/user/project/img/koding_edit-on-ide.png Binary files differnew file mode 100644 index 00000000000..fd5aaff75f5 --- /dev/null +++ b/doc/user/project/img/koding_edit-on-ide.png diff --git a/doc/user/project/img/koding_enable-koding.png b/doc/user/project/img/koding_enable-koding.png Binary files differnew file mode 100644 index 00000000000..c0ae0ee9918 --- /dev/null +++ b/doc/user/project/img/koding_enable-koding.png diff --git a/doc/user/project/img/koding_landing.png b/doc/user/project/img/koding_landing.png Binary files differnew file mode 100644 index 00000000000..7c629d9b05e --- /dev/null +++ b/doc/user/project/img/koding_landing.png diff --git a/doc/user/project/img/koding_open-gitlab-from-koding.png b/doc/user/project/img/koding_open-gitlab-from-koding.png Binary files differnew file mode 100644 index 00000000000..c958cf8f224 --- /dev/null +++ b/doc/user/project/img/koding_open-gitlab-from-koding.png diff --git a/doc/user/project/img/koding_run-in-ide.png b/doc/user/project/img/koding_run-in-ide.png Binary files differnew file mode 100644 index 00000000000..f91ee0f74cc --- /dev/null +++ b/doc/user/project/img/koding_run-in-ide.png diff --git a/doc/user/project/img/koding_run-mr-in-ide.png b/doc/user/project/img/koding_run-mr-in-ide.png Binary files differnew file mode 100644 index 00000000000..502817a2a46 --- /dev/null +++ b/doc/user/project/img/koding_run-mr-in-ide.png diff --git a/doc/user/project/img/koding_set-up-ide.png b/doc/user/project/img/koding_set-up-ide.png Binary files differnew file mode 100644 index 00000000000..7f408c980b5 --- /dev/null +++ b/doc/user/project/img/koding_set-up-ide.png diff --git a/doc/user/project/img/koding_stack-import.png b/doc/user/project/img/koding_stack-import.png Binary files differnew file mode 100644 index 00000000000..2a4e3c87fc8 --- /dev/null +++ b/doc/user/project/img/koding_stack-import.png diff --git a/doc/user/project/img/koding_start-build.png b/doc/user/project/img/koding_start-build.png Binary files differnew file mode 100644 index 00000000000..52159440f62 --- /dev/null +++ b/doc/user/project/img/koding_start-build.png diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png Binary files differindex 9c33db36586..812cc8767b7 100644 --- a/doc/user/project/img/protected_branches_devs_can_push.png +++ b/doc/user/project/img/protected_branches_devs_can_push.png diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png Binary files differindex 9f070f7a208..f33f1b2bdb6 100644 --- a/doc/user/project/img/protected_branches_list.png +++ b/doc/user/project/img/protected_branches_list.png diff --git a/doc/user/project/img/protected_branches_page.png b/doc/user/project/img/protected_branches_page.png Binary files differnew file mode 100644 index 00000000000..1585dde5b29 --- /dev/null +++ b/doc/user/project/img/protected_branches_page.png diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md new file mode 100644 index 00000000000..cac926b3e28 --- /dev/null +++ b/doc/user/project/issue_board.md @@ -0,0 +1,187 @@ +# Issue board + +> [Introduced][ce-5554] in GitLab 8.11. + +The GitLab Issue Board is a software project management tool used to plan, +organize, and visualize a workflow for a feature or product release. +It can be seen like a light version of a [Kanban] or a [Scrum] board. + +Other interesting links: + +- [GitLab Issue Board landing page on about.gitlab.com][landing] +- [YouTube video introduction to Issue Boards][youtube] + +## Overview + +The Issue Board builds on GitLab's existing issue tracking functionality and +leverages the power of [labels] by utilizing them as lists of the scrum board. + +With the Issue Board you can have a different view of your issues while also +maintaining the same filtering and sorting abilities you see across the +issue tracker. + +Below is a table of the definitions used for GitLab's Issue Board. + +| What we call it | What it means | +| -------------- | ------------- | +| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. | +| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. | +| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). | + +There are three types of lists, the ones you create based on your labels, and +two default: + +- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left. +- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right. +- Label list: a list based on a label. It shows all opened or closed issues with that label. + +![GitLab Issue Board](img/issue_board.png) + +--- + +In short, here's a list of actions you can take in an Issue Board: + +- [Create a new list](#creating-a-new-list). +- [Delete an existing list](#deleting-a-list). +- Drag issues between lists. +- Drag and reorder the lists themselves. +- Change issue labels on-the-fly while dragging issues between lists. +- Close an issue if you drag it to the **Done** list. +- Create a new list from a non-existing label by [creating the label on-the-fly](#creating-a-new-list) + within the Issue Board. +- [Filter issues](#filtering-issues) that appear across your Issue Board. + +If you are not able to perform one or more of the things above, make sure you +have the right [permissions](#permissions). + +## First time using the Issue Board + +The first time you navigate to your Issue Board, you will be presented with the +two default lists (**Backlog** and **Done**) and a welcoming message that gives +you two options. You can either create a predefined set of labels and create +their corresponding lists to the Issue Board or opt-out and use your own lists. + +![Issue Board welcome message](img/issue_board_welcome_message.png) + +If you choose to use and create the predefined lists, they will appear as empty +because the labels associated to them will not exist up until that moment, +which means the system has no way of populating them automatically. That's of +course if the predefined labels don't already exist. If any of them does exist, +the list will be created and filled with the issues that have that label. + +## Creating a new list + +Create a new list by clicking on the **Create new list** button at the upper +right corner of the Issue Board. + +![Issue Board welcome message](img/issue_board_add_list.png) + +Simply choose the label to create the list from. The new list will be inserted +at the end of the lists, before **Done**. Moving and reordering lists is as +easy as dragging them around. + +To create a list for a label that doesn't yet exist, simply create the label by +choosing **Create new label**. The label will be created on-the-fly and it will +be immediately added to the dropdown. You can now choose it to create a list. + +## Deleting a list + +To delete a list from the Issue Board use the small trash icon that is present +in the list's heading. A confirmation dialog will appear for you to confirm. + +Deleting a list doesn't have any effect in issues and labels, it's just the +list view that is removed. You can always add it back later if you need. + +## Searching issues in the Backlog list + +The very first time you start using the Issue Board, it is very likely your +issue tracker is already populated with labels and issues. In that case, +**Backlog** will have all the issues that don't belong to another list, and +**Done** will have all the closed ones. + +For performance and visibility reasons, each list shows the first 20 issues +by default. If you have more than 20, you have to start scrolling down for the +next 20 issues to appear. This can be cumbersome if your issue tracker hosts +hundreds of issues, and for that reason it is easier to search for issues to +move from **Backlog** to another list. + +Start typing in the search bar under the **Backlog** list and the relevant +issues will appear. + +![Issue Board search Backlog](img/issue_board_search_backlog.png) + +## Filtering issues + +You should be able to use the filters on top of your Issue Board to show only +the results you want. This is similar to the filtering used in the issue tracker +since the metadata from the issues and labels are re-used in the Issue Board. + +You can filter by author, assignee, milestone and label. + +## Creating workflows + +By reordering your lists, you can create workflows. As lists in Issue Boards are +based on labels, it works out of the box with your existing issues. So if you've +already labeled things with 'Backend' and 'Frontend', the issue will appear in +the lists as you create them. In addition, this means you can easily move +something between lists by changing a label. + +A typical workflow of using the Issue Board would be: + +1. You have [created][create-labels] and [prioritized][label-priority] labels + so that you can easily categorize your issues. +1. You have a bunch of issues (ideally labeled). +1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to + create a workflow. +1. You move issues around in lists so that your team knows who should be working + on what issue. +1. When the work by one team is done, the issue can be dragged to the next list + so someone else can pick up. +1. When the issue is finally resolved, the issue is moved to the **Done** list + and gets automatically closed. + +For instance you can create a list based on the label of 'Frontend' and one for +'Backend'. A designer can start working on an issue by dragging it from +**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being +worked on by the designers. Then, once they're done, all they have to do is +drag it over to the next list, 'Backend', where a backend developer can +eventually pick it up. Once they’re done, they move it to **Done**, to close the +issue. + +This process can be seen clearly when visiting an issue since with every move +to another list the label changes and a system not is recorded. + +![Issue Board system notes](img/issue_board_system_notes.png) + +## Permissions + +[Developers and up](../permissions.md) can use all the functionality of the +Issue Board, that is create/delete lists and drag issues around. + +## Tips + +A few things to remember: + +- The label that corresponds to a list is hidden for issues under that list. +- Moving an issue between lists removes the label from the list it came from + and adds the label from the list it goes to. +- When moving a card to **Done**, the label of the list it came from is removed + and the issue gets closed. +- An issue can exist in multiple lists if it has more than one label. +- Lists are populated with issues automatically if the issues are labeled. +- Clicking on the issue title inside a card will take you to that issue. +- Clicking on a label inside a card will quickly filter the entire Issue Board + and show only the issues from all lists that have that label. +- Issues inside lists are [ordered by priority][label-priority]. +- For performance and visibility reasons, each list shows the first 20 issues + by default. If you have more than 20 issues start scrolling down and the next + 20 will appear. + +[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554 +[labels]: ./labels.md +[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development) +[kanban]: https://en.wikipedia.org/wiki/Kanban_(development) +[create-labels]: ./labels.md#create-new-labels +[label-priority]: ./labels.md#prioritize-labels +[landing]: https://about.gitlab.com/solutions/issueboard +[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8 diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md new file mode 100644 index 00000000000..c56a1efe3c2 --- /dev/null +++ b/doc/user/project/koding.md @@ -0,0 +1,128 @@ +# Koding & GitLab + +> [Introduced][ce-5909] in GitLab 8.11. + +This document will guide you through using Koding integration on GitLab in +detail. For configuring and installing please follow the +[administrator guide](../../administration/integration/koding.md). + +You can use Koding integration to run and develop your projects on GitLab. This +will allow you and the users to test your project without leaving the browser. +Koding handles projects as stacks which are basic recipes to define your +environment for your project. With this integration you can automatically +create a proper stack template for your projects. Currently auto-generated +stack templates are designed to work with AWS which requires a valid AWS +credential to be able to use these stacks. You can find more information about +stacks and the other providers that you can use on Koding following the +[Koding documentation][koding-docs]. + +## Enable Integration + +You can enable Koding integration by providing the running Koding instance URL +in Application Settings under **Admin area > Settings** (`/admin/application_settings`). + +![Enable Koding](img/koding_enable-koding.png) + +Once enabled you will see `Koding` link on your sidebar which leads you to +Koding Landing page. + +![Koding Landing](img/koding_landing.png) + +You can navigate to running Koding instance from here. For more information and +details about configuring the integration, please follow the +[administrator guide](../../administration/integration/koding.md). + +## Set up Koding on Projects + +Once it's enabled, you will see some integration buttons on Project pages, +Merge Requests etc. To get started working on a specific project you first need +to create a `.koding.yml` file under your project root. You can easily do that +by using `Set Up Koding` button which will be visible on every project's +landing page; + +![Set Up Koding](img/koding_set-up-ide.png) + +Once you click this will open a New File page on GitLab with auto-generated +`.koding.yml` content based on your server and repository configuration. + +![Commit .koding.yml](img/koding_commit-koding.yml.png) + + +## Run a project on Koding + +If there is `.koding.yml` exists in your project root, you will see +`Run in IDE (Koding)` button in your project landing page. You can initiate the +process from here. + +![Run on Koding](img/koding_run-in-ide.png) + +This will open Koding defined in the settings in a new window and will start +importing the project's stack file. + +![Import Stack](img/koding_stack-import.png) + +You should see the details of your repository imported into your Koding +instance. Once it's completed it will lead you to the Stack Editor and from +there you can start using your new stack integrated with your project on your +GitLab instance. For details about what's next you can follow +[this guide](https://www.koding.com/docs/creating-an-aws-stack) from step 8. + +Once stack initialized you will see the `README.md` content from your project +in `Stack Build` wizard, this wizard will let you build the stack and import +your project into it. **Once it's completed it will automatically open the +related vm instead of importing from scratch**. + +![Stack Building](img/koding_start-build.png) + +This will take time depending on the required environment. + +![Stack Building in Progress](img/koding_build-in-progress.png) + +It usually takes ~4 min. to make it ready with a `t2.nano` instance on given +AWS region. (`t2.nano` is default vm type on auto-generated stack template +which can be manually changed). + +![Stack Building Success](img/koding_build-success.png) + +You can check out the `Build Logs` from this success modal as well. + +![Stack Build Logs](img/koding_build-logs.png) + +You can now `Start Coding`! + +![Edit On IDE](img/koding_edit-on-ide.png) + +## Try a Merge Request on IDE + +It's also possible to try a change on IDE before merging it. This flow only +enabled if the target project has `.koding.yml` in it's target branch. You +should see the alternative version of `Run in IDE (Koding)` button in merge +request pages as well; + +![Run in IDE on MR](img/koding_run-mr-in-ide.png) + +This will again take you to Koding with proper arguments passed, which will +allow Koding to modify the stack template provided by target branch. You can +see the difference; + +![Different Branch for MR](img/koding_different-stack-on-mr-try.png) + +The flow for the branch stack is also same with the regular project flow. + +## Open GitLab from Koding + +Since stacks generated with import flow defined in previous steps, they have +information about the repository they are belonging to. By using this +information you can access to related GitLab page from stacks on your sidebar +on Koding. + +![Open GitLab from Koding](img/koding_open-gitlab-from-koding.png) + +## Other links + +- [YouTube video on GitLab + Koding workflow][youtube] +- [Koding documentation][koding-docs] + +[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909 +[youtube]: https://youtu.be/3wei5yv_Ye8 +[koding-docs]: https://www.koding.com/docs diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index 44b76ffc8e6..4d7225bd820 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -26,6 +26,7 @@ this is similar to performing `git checkout feature; git merge master` locally. GitLab allows resolving conflicts in a file where all of the below are true: - The file is text, not binary +- The file is in a UTF-8 compatible encoding - The file does not already contain conflict markers - The file, with conflict markers added, is not over 200 KB in size - The file exists under the same path in both branches diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 96d9bdc1b29..f7a686d2ccf 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -5,6 +5,8 @@ idea of having read or write permission to the repository and branches. To prevent people from messing with history or pushing code without review, we've created protected branches. +## Overview + By default, a protected branch does four simple things: - it prevents its creation, if not already created, from everybody except users @@ -15,6 +17,11 @@ By default, a protected branch does four simple things: See the [Changelog](#changelog) section for changes over time. +> +>Additional functionality for GitLab Enterprise Edition: +> +>- Restrict push and merge access to [certain users][ee-restrict] + ## Configuring protected branches To protect a branch, you need to have at least Master permission level. Note @@ -28,22 +35,41 @@ that the `master` branch is protected by default. 1. From the **Branch** dropdown menu, select the branch you want to protect and click **Protect**. In the screenshot below, we chose the `develop` branch. - ![Choose protected branch](img/protected_branches_choose_branch.png) + ![Protected branches page](img/protected_branches_page.png) -1. Once done, the protected branch will appear in the "Already protected" list. +1. Once done, the protected branch will appear in the "Protected branches" list. ![Protected branches list](img/protected_branches_list.png) +## Using the Allowed to merge and Allowed to push settings + +> [Introduced][ce-5081] in GitLab 8.11. + +Since GitLab 8.11, we added another layer of branch protection which provides +more granular management of protected branches. The "Developers can push" +option was replaced by an "Allowed to push" setting which can be set to +allow/prohibit Masters and/or Developers to push to a protected branch. + +Using the "Allowed to push" and "Allowed to merge" settings, you can control +the actions that different roles can perform with the protected branch. +For example, you could set "Allowed to push" to "No one", and "Allowed to merge" +to "Developers + Masters", to require _everyone_ to submit a merge request for +changes going into the protected branch. This is compatible with workflows like +the [GitLab workflow](../../workflow/gitlab_flow.md). + +However, there are workflows where that is not needed, and only protecting from +force pushes and branch removal is useful. For those workflows, you can allow +everyone with write access to push to a protected branch by setting +"Allowed to push" to "Developers + Masters". + +You can set the "Allowed to push" and "Allowed to merge" options while creating +a protected branch or afterwards by selecting the option you want from the +dropdown list in the "Already protected" area. -Since GitLab 8.10, we added another layer of branch protection which provides -more granular management of protected branches. You can now choose the option -"Developers can merge" so that Developer users can merge a merge request but -not directly push. In that case, your branches are protected from direct pushes, -yet Developers don't need elevated permissions or wait for someone with a higher -permission level to press merge. +![Developers can push](img/protected_branches_devs_can_push.png) -You can set this option while creating the protected branch or after its -creation. +If you don't choose any of those options while creating a protected branch, +they are set to "Masters" by default. ## Wildcard protected branches @@ -66,40 +92,25 @@ Two different wildcards can potentially match the same branch. For example, In that case, if _any_ of these protected branches have a setting like "Allowed to push", then `production-stable` will also inherit this setting. -If you click on a protected branch's name that is created using a wildcard, -you will be presented with a list of all matching branches: +If you click on a protected branch's name, you will be presented with a list of +all matching branches: ![Protected branch matches](img/protected_branches_matches.png) -## Restrict the creation of protected branches - -Creating a protected branch or a list of protected branches using the wildcard -feature, not only you are restricting pushes to those branches, but also their -creation if not already created. - -## Error messages when pushing to a protected branch - -A user with insufficient permissions will be presented with an error when -creating or pushing to a branch that's prohibited, either through GitLab's UI: - -![Protected branch error GitLab UI](img/protected_branches_error_ui.png) - -or using Git from their terminal: +## Changelog -```bash -remote: GitLab: You are not allowed to push code to protected branches on this project. -To https://gitlab.example.com/thedude/bowling.git - ! [remote rejected] staging-stable -> staging-stable (pre-receive hook declined) -error: failed to push some refs to 'https://gitlab.example.com/thedude/bowling.git' -``` +**8.11** -## Changelog +- Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081] -**8.10.0** +**8.10** -- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!5081][ce-4665] +- Allow developers to merge into a protected branch without having push access [gitlab-org/gitlab-ce!4892][ce-4892] +- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!4665][ce-4665] --- [ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards" +[ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access" [ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to" +[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users diff --git a/doc/workflow/slash_commands.md b/doc/user/project/slash_commands.md index 91d69d4e77e..1792a0c501d 100644 --- a/doc/workflow/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -26,5 +26,5 @@ do. | `/done` | Mark todo as done | | `/subscribe` | Subscribe | | `/unsubscribe` | Unsubscribe | -| `/due <in 2 days | this Friday | December 31st>` | Set due date | +| <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | | `/remove_due_date` | Remove due date | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 1653d95e722..0cf56449de2 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -6,8 +6,8 @@ - [Feature branch workflow](workflow.md) - [GitLab Flow](gitlab_flow.md) - [Groups](groups.md) +- [Issue Board](../user/project/issue_board.md) - [Keyboard shortcuts](shortcuts.md) -- [Slash commands](slash_commands.md) - [File finder](file_finder.md) - [Labels](../user/project/labels.md) - [Notification emails](notifications.md) @@ -15,6 +15,7 @@ - [Project forking workflow](forking_workflow.md) - [Project users](add-user/add-user.md) - [Protected branches](../user/project/protected_branches.md) +- [Slash commands](../user/project/slash_commands.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) - [Web Editor](web_editor.md) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 2b2f140f8bf..7c0eb90d540 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -89,7 +89,7 @@ In this case the master branch is deployed on staging. When someone wants to dep And going live with code happens by merging the pre-production branch into the production branch. This workflow where commits only flow downstream ensures that everything has been tested on all environments. If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch. -If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches. +If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches. If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches. An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/). @@ -115,7 +115,7 @@ In this flow it is not common to have a production branch (or git flow master br Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. -Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee. +Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee. In this article we'll refer to them as merge requests. If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team. diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index d2ec56e6504..40a5e4476be 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -15,6 +15,25 @@ Please note that you need to have builds configured to enable this feature. ## Checkout merge requests locally +### By adding a git alias + +Add the following alias to your `~/.gitconfig`: + +``` +[alias] + mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' - +``` + +Now you can check out a particular merge request from any repository and any remote, e.g. to check out a merge request number 5 as shown in GitLab from the `upstream` remote, do: + +``` +$ git mr upstream 5 +``` + +This will fetch the merge request into a local `mr-upstream-5` branch and check it out. + +### By modifying `.git/config` for a given repository + Locate the section for your GitLab remote in the `.git/config` file. It looks like this: ``` @@ -34,7 +53,7 @@ It should look like this: fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/* ``` -Now you can fetch all the merge requests requests: +Now you can fetch all the merge requests: ``` $ git fetch origin @@ -61,3 +80,11 @@ If you click the "Hide whitespace changes" button, you can see the diff without It is also working on commits compare view. ![Commit Compare](merge_requests/commit_compare.png) + +## Merge Requests versions + +Every time you push to merge request branch, a new version of merge request diff +is created. When you visit the merge request page you see latest version of changes. +However you can select an older one from version dropdown + +![Merge Request Versions](merge_requests/versions.png) diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png Binary files differnew file mode 100644 index 00000000000..c0a6dfe6806 --- /dev/null +++ b/doc/workflow/merge_requests/versions.png diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md index a523b3facbe..f19e7df8c9a 100644 --- a/doc/workflow/project_features.md +++ b/doc/workflow/project_features.md @@ -32,4 +32,12 @@ Snippets are little bits of code or text. This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control. -For example, a specific config file that is used by > the team that is only valid for the people that work on the code. +For example, a specific config file that is used by the team that is only valid for the people that work on the code. + +## Git LFS + +>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins. + +Git Large File Storage allows you to easily manage large binary files with Git. +With this setting admins can better control which projects are allowed to use +LFS. diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md index 4c59f59c587..8e50cb03e63 100644 --- a/doc/workflow/share_projects_with_other_groups.md +++ b/doc/workflow/share_projects_with_other_groups.md @@ -1,22 +1,24 @@ # Share Projects with other Groups -In GitLab Enterprise Edition you can share projects with other groups. -This makes it possible to add a group of users to a project with a single action. +You can share projects with other groups. This makes it possible to add a group of users +to a project with a single action. ## Groups as collections of users -In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md). -In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members. +Groups are used primarily to [create collections of projects](groups.md), but you can also +take advantage of the fact that groups define collections of _users_, namely the group +members. ## Sharing a project with a group of users -The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'. -But what if 'Project Acme' already belongs to another group, say 'Open Source'? -This is where the (Enterprise Edition only) group sharing feature can be of use. +The primary mechanism to give a group of users, say 'Engineering', access to a project, +say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project +Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? +This is where the group sharing feature can be of use. To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. -![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png) +![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png) Now you can add the 'Engineering' group with the maximum access level of your choice. After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 2c17d32154a..88fef674c0c 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -22,6 +22,7 @@ Feature: Project Commits Branches @javascript Scenario: I delete a branch Given I visit project branches page + And I filter for branch improve/awesome And I click branch 'improve/awesome' delete link Then I should not see branch 'improve/awesome' diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 6bac6011467..5aa592e9067 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -24,7 +24,7 @@ Feature: Project Merge Requests Scenario: I should see target branch when it is different from default Given project "Shop" have "Bug NS-06" open merge request When I visit project "Shop" merge requests page - Then I should see "other_branch" branch + Then I should see "feature_conflict" branch Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target Given project "Shop" have "Bug NS-07" open merge request with rebased branch @@ -89,7 +89,7 @@ Feature: Project Merge Requests Then The list should be sorted by "Oldest updated" @javascript - Scenario: Visiting Merge Requests from a differente Project after sorting + Scenario: Visiting Merge Requests from a different Project after sorting Given I visit project "Shop" merge requests page And I sort the list by "Oldest updated" And I visit dashboard merge requests page diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index f0d8d498e46..2f0941e4113 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -18,7 +18,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(page).to have_link('GitHub') expect(page).to have_link('Bitbucket') expect(page).to have_link('GitLab.com') - expect(page).to have_link('Gitorious.org') expect(page).to have_link('Google Code') expect(page).to have_link('Repo by URL') end diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index dfa2fa75def..e9b45823c67 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps member = mary_jane_member page.within "#group_member_#{member.id}" do - click_button "Edit access level" - select 'Developer', from: 'group_member_access_level' + click_button 'Edit' + select 'Developer', from: "member_access_level_#{member.id}" click_on 'Save' end end diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 4bfb7e92e99..5f9b9e0445e 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -73,6 +73,11 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps expect(page).to have_content 'Branch already exists' end + step 'I filter for branch improve/awesome' do + fill_in 'branch-search', with: 'improve/awesome' + find('#branch-search').native.send_keys(:enter) + end + step "I click branch 'improve/awesome' delete link" do page.within '.js-branch-improve\/awesome' do find('.btn-remove').click diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 9778ff4a6c7..56b28949585 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -58,8 +58,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(find('.merge-request-info')).not_to have_content "master" end - step 'I should see "other_branch" branch' do - expect(page).to have_content "other_branch" + step 'I should see "feature_conflict" branch' do + expect(page).to have_content "feature_conflict" end step 'I should see "Bug NS-04" in merge requests' do @@ -124,7 +124,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps source_project: project, target_project: project, source_branch: 'fix', - target_branch: 'other_branch', + target_branch: 'feature_conflict', author: project.users.first, description: "# Description header" ) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 841d191d55b..bb79424ee08 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see its content with new lines preserved at end of file' do - expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n" + expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n" end step 'I click link "Raw"' do @@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I can edit code' do set_new_content - expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content + expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content end step 'I edit code' do @@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I edit code with new lines at end of file' do - execute_script('blob.editor.setValue("Sample\n\n\n")') + execute_script('ace.edit("editor").setValue("Sample\n\n\n")') end step 'I fill the new file name' do @@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps private def set_new_content - execute_script("blob.editor.setValue('#{new_gitignore_content}')") + execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')") end # Content of the gitignore file on the seed repository. diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index f32576d2cb1..e920f5a706b 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) page.within "#project_member_#{project_member.id}" do - click_button "Edit access level" - select "Reporter", from: "project_member_access_level" + click_button 'Edit' + select "Reporter", from: "member_access_level_#{project_member.id}" click_button "Save" end end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index aa666a954bc..df9845ba569 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -179,7 +179,7 @@ module SharedIssuable project = Project.find_by(name: from_project_name) expect(page).to have_content(user_name) - expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") + expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") end def expect_sidebar_content(content) diff --git a/lib/api/api.rb b/lib/api/api.rb index 6b8bfbbdae6..4602e627fdb 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -18,22 +18,14 @@ module API end rescue_from :all do |exception| - # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 - # why is this not wrapped in something reusable? - trace = exception.backtrace - - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << trace.join("\n ") - - API.logger.add Logger::FATAL, message - rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) + handle_api_exception(exception) end format :json content_type :txt, "text/plain" # Ensure the namespace is right, otherwise we might load Grape::API::Helpers + helpers ::SentryHelper helpers ::API::Helpers mount ::API::AccessRequests @@ -75,5 +67,6 @@ module API mount ::API::Triggers mount ::API::Users mount ::API::Variables + mount ::API::MergeRequestDiffs end end diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 2efe7e3adf3..7c22b17e4e5 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -54,7 +54,7 @@ module API post endpoint do required_attributes! [:name] - not_found!('Award Emoji') unless can_read_awardable? + not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? award = awardable.create_award_emoji(params[:name], current_user) @@ -92,6 +92,10 @@ module API can?(current_user, ability, awardable) end + def can_award_awardable? + awardable.user_can_award?(current_user, params[:name]) + end + def awardable @awardable ||= begin diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 4df6ca8333e..5e3c9563703 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -64,7 +64,7 @@ module API ref = branches.first end - pipeline = @project.ensure_pipeline(commit.sha, ref, current_user) + pipeline = @project.ensure_pipeline(ref, commit.sha, current_user) name = params[:name] || params[:context] status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 67420772335..4335e3055ef 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -49,7 +49,7 @@ module API class ProjectHook < Hook expose :project_id, :push_events expose :issues_events, :merge_requests_events, :tag_push_events - expose :note_events, :build_events, :pipeline_events + expose :note_events, :build_events, :pipeline_events, :wiki_page_events expose :enable_ssl_verification end @@ -78,7 +78,7 @@ module API expose :path, :path_with_namespace expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled expose :created_at, :last_activity_at - expose :shared_runners_enabled + expose :shared_runners_enabled, :lfs_enabled expose :creator_id expose :namespace expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } @@ -90,6 +90,7 @@ module API expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links.all, options) end + expose :only_allow_merge_if_build_succeeds end class Member < UserBasic @@ -97,6 +98,10 @@ module API member = options[:member] || options[:members].find { |m| m.user_id == user.id } member.access_level end + expose :expires_at do |user, options| + member = options[:member] || options[:members].find { |m| m.user_id == user.id } + member.expires_at + end end class AccessRequester < UserBasic @@ -173,6 +178,10 @@ module API # TODO (rspeicher): Deprecated; remove in 9.0 expose(:expires_at) { |snippet| nil } + + expose :web_url do |snippet, options| + Gitlab::UrlBuilder.build(snippet) + end end class ProjectEntity < Grape::Entity @@ -202,6 +211,10 @@ module API expose :user_notes_count expose :upvotes, :downvotes expose :due_date + + expose :web_url do |issue, options| + Gitlab::UrlBuilder.build(issue) + end end class ExternalIssue < Grape::Entity @@ -225,6 +238,10 @@ module API expose :user_notes_count expose :should_remove_source_branch?, as: :should_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch + + expose :web_url do |merge_request, options| + Gitlab::UrlBuilder.build(merge_request) + end end class MergeRequestChanges < MergeRequest @@ -233,6 +250,19 @@ module API end end + class MergeRequestDiff < Grape::Entity + expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha, + :created_at, :merge_request_id, :state, :real_size + end + + class MergeRequestDiffFull < MergeRequestDiff + expose :commits, using: Entities::RepoCommit + + expose :diffs, using: Entities::RepoDiff do |compare, _| + compare.raw_diffs(all_diffs: true).to_a + end + end + class SSHKey < Grape::Entity expose :id, :title, :key, :created_at end @@ -510,13 +540,12 @@ module API expose :duration end - class Environment < Grape::Entity + class EnvironmentBasic < Grape::Entity expose :id, :name, :external_url - expose :project, using: Entities::Project end - class EnvironmentBasic < Grape::Entity - expose :id, :name, :external_url + class Environment < EnvironmentBasic + expose :project, using: Entities::Project end class Deployment < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d0469d6602d..da4b1bf9902 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -279,6 +279,24 @@ module API error!({ 'message' => message }, status) end + def handle_api_exception(exception) + if sentry_enabled? && report_exception?(exception) + define_params_for_grape_middleware + sentry_context + Raven.capture_exception(exception) + end + + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 + trace = exception.backtrace + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + + API.logger.add Logger::FATAL, message + rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) + end + # Projects helpers def filter_projects(projects) @@ -419,5 +437,19 @@ module API Entities::Issue end end + + # The Grape Error Middleware only has access to env but no params. We workaround this by + # defining a method that returns the right value. + def define_params_for_grape_middleware + self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys } + end + + # We could get a Grape or a standard Ruby exception. We should only report anything that + # is clearly an error. + def report_exception?(exception) + return true unless exception.respond_to?(:status) + + exception.status == 500 + end end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index d8e9ac406c4..5b54c11ef62 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -101,6 +101,31 @@ module API {} end end + + post '/two_factor_recovery_codes' do + status 200 + + key = Key.find(params[:key_id]) + user = key.user + + # Make sure this isn't a deploy key + unless key.type.nil? + return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } + end + + unless user.present? + return { success: false, message: 'Could not find a user for the given key' } + end + + unless user.two_factor_enabled? + return { success: false, message: 'Two-factor authentication is not enabled for this user' } + end + + codes = user.generate_otp_backup_codes! + user.save! + + { success: true, recovery_codes: codes } + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 077258faee1..d0bc7243e54 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -154,21 +154,15 @@ module API render_api_error!({ labels: errors }, 400) end - project = user_project + attrs[:labels] = params[:labels] if params[:labels] - issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute + issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end if issue.valid? - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels].present? - issue.add_labels_by_names(params[:labels].split(',')) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) @@ -202,17 +196,11 @@ module API render_api_error!({ labels: errors }, 400) end + attrs[:labels] = params[:labels] if params[:labels] + issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) if issue.valid? - # Find or create labels and attach to issue. Labels are valid because - # we already checked its name, so there can't be an error here - if params[:labels] && can?(current_user, :admin_issue, user_project) - issue.remove_labels - # Create and add labels to the new created issue - issue.add_labels_by_names(params[:labels].split(',')) - end - present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) diff --git a/lib/api/members.rb b/lib/api/members.rb index 2fae83f60b2..94c16710d9a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -49,6 +49,7 @@ module API # id (required) - The group/project ID # user_id (required) - The user ID of the new member # access_level (required) - A valid access level + # expires_at (optional) - Date string in the format YEAR-MONTH-DAY # # Example Request: # POST /groups/:id/members @@ -72,7 +73,7 @@ module API conflict!('Member already exists') if source_type == 'group' && member unless member - source.add_user(params[:user_id], params[:access_level], current_user) + source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) member = source.members.find_by(user_id: params[:user_id]) end @@ -81,7 +82,7 @@ module API else # Since `source.add_user` doesn't return a member object, we have to # build a new one and populate its errors in order to render them. - member = source.members.build(attributes_for_keys([:user_id, :access_level])) + member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at])) member.valid? # populate the errors # This is to ensure back-compatibility but 400 behavior should be used @@ -97,6 +98,7 @@ module API # id (required) - The group/project ID # user_id (required) - The user ID of the member # access_level (required) - A valid access level + # expires_at (optional) - Date string in the format YEAR-MONTH-DAY # # Example Request: # PUT /groups/:id/members/:user_id @@ -107,8 +109,9 @@ module API required_attributes! [:user_id, :access_level] member = source.members.find_by!(user_id: params[:user_id]) + attrs = attributes_for_keys [:access_level, :expires_at] - if member.update_attributes(access_level: params[:access_level]) + if member.update_attributes(attrs) present member.user, with: Entities::Member, member: member else # This is to ensure back-compatibility but 400 behavior should be used diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb new file mode 100644 index 00000000000..07435d78468 --- /dev/null +++ b/lib/api/merge_request_diffs.rb @@ -0,0 +1,45 @@ +module API + # MergeRequestDiff API + class MergeRequestDiffs < Grape::API + before { authenticate! } + + resource :projects do + desc 'Get a list of merge request diff versions' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::MergeRequestDiff + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + + get ":id/merge_requests/:merge_request_id/versions" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + + authorize! :read_merge_request, merge_request + present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff + end + + desc 'Get a single merge request diff version' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::MergeRequestDiffFull + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' + end + + get ":id/merge_requests/:merge_request_id/versions/:version_id" do + merge_request = user_project.merge_requests. + find(params[:merge_request_id]) + + authorize! :read_merge_request, merge_request + present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull + end + end + end +end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 3f63cd678e8..14f5be3b5f6 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -46,6 +46,7 @@ module API :note_events, :build_events, :pipeline_events, + :wiki_page_events, :enable_ssl_verification ] @hook = user_project.hooks.new(attrs) @@ -80,6 +81,7 @@ module API :note_events, :build_events, :pipeline_events, + :wiki_page_events, :enable_ssl_verification ] diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 60cfc103afd..f8979a1cc29 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -105,6 +105,7 @@ module API # visibility_level (optional) - 0 by default # import_url (optional) # public_builds (optional) + # lfs_enabled (optional) # Example Request # POST /projects post do @@ -123,7 +124,9 @@ module API :public, :visibility_level, :import_url, - :public_builds] + :public_builds, + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -155,6 +158,7 @@ module API # visibility_level (optional) # import_url (optional) # public_builds (optional) + # lfs_enabled (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do @@ -172,7 +176,9 @@ module API :public, :visibility_level, :import_url, - :public_builds] + :public_builds, + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -218,6 +224,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project # public_builds (optional) + # lfs_enabled (optional) # Example Request # PUT /projects/:id put ':id' do @@ -234,7 +241,9 @@ module API :shared_runners_enabled, :public, :visibility_level, - :public_builds] + :public_builds, + :only_allow_merge_if_build_succeeds, + :lfs_enabled] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index f117fc3d37d..9fcd9a3f999 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -55,7 +55,7 @@ module Backup bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s) FileUtils.mv(path, bk_repos_path) # This is expected from gitlab:check - FileUtils.mkdir_p(path, mode: 2770) + FileUtils.mkdir_p(path, mode: 02770) end Project.find_each(batch_size: 1000) do |project| diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 17bb99a2ae5..a6b9beecded 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -9,22 +9,14 @@ module Ci end rescue_from :all do |exception| - # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 - # why is this not wrapped in something reusable? - trace = exception.backtrace - - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << trace.join("\n ") - - API.logger.add Logger::FATAL, message - rack_response({ 'message' => '500 Internal Server Error' }, 500) + handle_api_exception(exception) end content_type :txt, 'text/plain' content_type :json, 'application/json' format :json + helpers ::SentryHelper helpers ::Ci::API::Helpers helpers ::API::Helpers helpers Gitlab::CurrentSettings diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 84688f6646e..a4558d157c0 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -94,7 +94,7 @@ module ExtractsPath @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) - @id = Addressable::URI.normalize_component(get_id) + @id = get_id @ref, @path = extract_ref(@id) @repo = @project.repository if @options[:extended_sha1].blank? @@ -119,6 +119,7 @@ module ExtractsPath private + # overriden in subclasses, do not remove def get_id id = params[:id] || params[:ref] id += "/" + params[:path] unless params[:path].blank? diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 3d56ea3e47a..9a0482306b7 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -12,10 +12,7 @@ module Gitlab @ref = ref @job = job - @pipeline = @project.pipelines - .where(ref: @ref) - .where(sha: @project.commit(@ref).try(:sha)) - .first + @pipeline = @project.pipelines.latest_successful_for(@ref) end def entity diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb index 073044b66f8..fe4ee8a7fc6 100644 --- a/lib/gitlab/ci/config/node/hidden_job.rb +++ b/lib/gitlab/ci/config/node/hidden.rb @@ -5,11 +5,10 @@ module Gitlab ## # Entry that represents a hidden CI/CD job. # - class HiddenJob < Entry + class Hidden < Entry include Validatable validations do - validates :config, type: Hash validates :config, presence: true end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 51683c82ceb..a1a26d4fd8f 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -30,7 +30,7 @@ module Gitlab def compose! @config.each do |name, config| - node = hidden?(name) ? Node::HiddenJob : Node::Job + node = hidden?(name) ? Node::Hidden : Node::Job factory = Node::Factory.new(node) .value(config || {}) diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 0a1fd27ced5..dff9e29c6a5 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -181,6 +181,17 @@ module Gitlab sections: sections } end + + # Don't try to print merge_request or repository. + def inspect + instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable| + value = instance_variable_get("@#{instance_variable}") + + "#{instance_variable}=\"#{value}\"" + end + + "#<#{self.class} #{instance_variables.join(' ')}>" + end end end end diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index 6eccded7872..2d4d55daeeb 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -13,10 +13,19 @@ module Gitlab class UnmergeableFile < ParserError end + class UnsupportedEncoding < ParserError + end + def parse(text, our_path:, their_path:, parent_file: nil) raise UnmergeableFile if text.blank? # Typically a binary file raise UnmergeableFile if text.length > 102400 + begin + text.to_json + rescue Encoding::UndefinedConversionError + raise UnsupportedEncoding + end + line_obj_index = 0 line_old = 1 line_new = 1 diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 9dc2602867e..bd681f03173 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -23,7 +23,6 @@ module Gitlab dates.each do |date| date_id = date.to_time.to_i.to_s - @timestamps[date_id] = 0 day_events = events.find { |day_events| day_events["date"] == date } if day_events diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 735331df66c..12fbb78c53e 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -30,6 +30,7 @@ module Gitlab signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], + koding_enabled: false, sign_in_text: nil, after_sign_up_text: nil, help_page_text: nil, @@ -40,7 +41,7 @@ module Gitlab default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], domain_whitelist: Settings.gitlab['domain_whitelist'], - import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project], + import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 4f946908e2f..36348b33943 100644 --- a/lib/gitlab/diff/file_collection/merge_request.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -1,14 +1,14 @@ module Gitlab module Diff module FileCollection - class MergeRequest < Base - def initialize(merge_request, diff_options:) - @merge_request = merge_request + class MergeRequestDiff < Base + def initialize(merge_request_diff, diff_options:) + @merge_request_diff = merge_request_diff - super(merge_request, - project: merge_request.project, + super(merge_request_diff, + project: merge_request_diff.project, diff_options: diff_options, - diff_refs: merge_request.diff_refs) + diff_refs: merge_request_diff.diff_refs) end def diff_files @@ -61,11 +61,11 @@ module Gitlab end def cacheable? - @merge_request.merge_request_diff.present? + @merge_request_diff.present? end def cache_key - [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options] + [@merge_request_diff, 'highlighted-diff-files', diff_options] end end end diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index 2fdcf8d7838..ecf62dead35 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -139,13 +139,19 @@ module Gitlab private def find_diff_file(repository) - diffs = Gitlab::Git::Compare.new( - repository.raw_repository, - start_sha, - head_sha - ).diffs(paths: paths) + # We're at the initial commit, so just get that as we can't compare to anything. + if Gitlab::Git.blank_ref?(start_sha) + compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha) + else + compare = Gitlab::Git::Compare.new( + repository.raw_repository, + start_sha, + head_sha + ) + end + + diff = compare.diffs(paths: paths).first - diff = diffs.first return unless diff Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs) diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb index bd3267e2a80..5cf9d5ebe28 100644 --- a/lib/gitlab/email/handler.rb +++ b/lib/gitlab/email/handler.rb @@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler' module Gitlab module Email module Handler - HANDLERS = [CreateNoteHandler, CreateIssueHandler] + # The `CreateIssueHandler` feature is disabled for the time being. + HANDLERS = [CreateNoteHandler] def self.for(mail, mail_key) HANDLERS.find do |klass| diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9ddc8905bd6..02ffb43d89b 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,13 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :client, :project, :repo, :repo_url + attr_reader :client, :errors, :project, :repo, :repo_url def initialize(project) @project = project @repo = project.import_source @repo_url = project.import_url + @errors = [] if credentials @client = Client.new(credentials[:user]) @@ -18,8 +19,14 @@ module Gitlab end def execute - import_labels && import_milestones && import_issues && - import_pull_requests && import_wiki + import_labels + import_milestones + import_issues + import_pull_requests + import_wiki + handle_errors + + true end private @@ -28,22 +35,37 @@ module Gitlab @credentials ||= project.import_data.credentials if project.import_data end + def handle_errors + return unless errors.any? + + project.update_column(:import_error, { + message: 'The remote data could not be fully imported.', + errors: errors + }.to_json) + end + def import_labels labels = client.labels(repo, per_page: 100) - labels.each { |raw| LabelFormatter.new(project, raw).create! } - true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message + labels.each do |raw| + begin + LabelFormatter.new(project, raw).create! + rescue => e + errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end + end end def import_milestones milestones = client.milestones(repo, state: :all, per_page: 100) - milestones.each { |raw| MilestoneFormatter.new(project, raw).create! } - true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message + milestones.each do |raw| + begin + MilestoneFormatter.new(project, raw).create! + rescue => e + errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end + end end def import_issues @@ -53,15 +75,15 @@ module Gitlab gh_issue = IssueFormatter.new(project, raw) if gh_issue.valid? - issue = gh_issue.create! - apply_labels(issue) - import_comments(issue) if gh_issue.has_comments? + begin + issue = gh_issue.create! + apply_labels(issue) + import_comments(issue) if gh_issue.has_comments? + rescue => e + errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end - - true - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message end def import_pull_requests @@ -77,14 +99,12 @@ module Gitlab apply_labels(merge_request) import_comments(merge_request) import_comments_on_diff(merge_request) - rescue ActiveRecord::RecordInvalid => e - raise Projects::ImportService::Error, e.message + rescue => e + errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } ensure clean_up_restored_branches(pull_request) end end - - true end def restore_source_branch(pull_request) @@ -98,7 +118,7 @@ module Gitlab def remove_branch(name) project.repository.delete_branch(name) rescue Rugged::ReferenceError - nil + errors << { type: :remove_branch, name: name } end def clean_up_restored_branches(pull_request) @@ -112,9 +132,10 @@ module Gitlab issue = client.issue(repo, issuable.iid) if issue.labels.count > 0 - label_ids = issue.labels.map do |raw| - Label.find_by(LabelFormatter.new(project, raw).attributes).try(:id) - end + label_ids = issue.labels + .map { |raw| LabelFormatter.new(project, raw).attributes } + .map { |attrs| Label.find_by(attrs).try(:id) } + .compact issuable.update_attribute(:label_ids, label_ids) end @@ -132,8 +153,12 @@ module Gitlab def create_comments(issuable, comments) comments.each do |raw| - comment = CommentFormatter.new(project, raw) - issuable.notes.create!(comment.attributes) + begin + comment = CommentFormatter.new(project, raw) + issuable.notes.create!(comment.attributes) + rescue => e + errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } + end end end @@ -143,16 +168,12 @@ module Gitlab gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) project.update_attribute(:wiki_enabled, true) end - - true rescue Gitlab::Shell::Error => e # GitHub error message when the wiki repo has not been created, # this means that repo has wiki enabled, but have no pages. So, # we can skip the import. if e.message !~ /repository not exported/ - raise Projects::ImportService::Error, e.message - else - true + errors << { type: :wiki, errors: e.message } end end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index b84538a090a..04aa3664f64 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -56,6 +56,10 @@ module Gitlab end end + def url + raw_data.url + end + private def assigned? diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb deleted file mode 100644 index 8d0132a744c..00000000000 --- a/lib/gitlab/gitorious_import.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Gitlab - module GitoriousImport - GITORIOUS_HOST = "https://gitorious.org" - end -end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb deleted file mode 100644 index 99fe5bdebfc..00000000000 --- a/lib/gitlab/gitorious_import/client.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Gitlab - module GitoriousImport - class Client - attr_reader :repo_list - - def initialize(repo_list) - @repo_list = repo_list - end - - def authorize_url(redirect_uri) - "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}" - end - - def repos - @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) } - end - - def repo(id) - repos.find { |repo| repo.id == id } - end - - private - - def repo_names - repo_list.to_s.split(',').map(&:strip).reject(&:blank?) - end - end - end -end diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb deleted file mode 100644 index 8e22aa9286d..00000000000 --- a/lib/gitlab/gitorious_import/project_creator.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab - module GitoriousImport - class ProjectCreator - attr_reader :repo, :namespace, :current_user - - def initialize(repo, namespace, current_user) - @repo = repo - @namespace = namespace - @current_user = current_user - end - - def execute - ::Projects::CreateService.new( - current_user, - name: repo.name, - path: repo.path, - description: repo.description, - namespace_id: namespace.id, - visibility_level: Gitlab::VisibilityLevel::PUBLIC, - import_type: "gitorious", - import_source: repo.full_name, - import_url: repo.import_url - ).execute - end - end - end -end diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb deleted file mode 100644 index c88f1ae358d..00000000000 --- a/lib/gitlab/gitorious_import/repository.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Gitlab - module GitoriousImport - Repository = Struct.new(:full_name) do - def id - Digest::SHA1.hexdigest(full_name) - end - - def namespace - segments.first - end - - def path - segments.last - end - - def name - path.titleize - end - - def description - "" - end - - def import_url - "#{GITORIOUS_HOST}/#{full_name}.git" - end - - private - - def segments - full_name.split('/') - end - end - end -end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 59a05411fe9..94261b7eeed 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -14,13 +14,12 @@ module Gitlab def options { - 'GitHub' => 'github', - 'Bitbucket' => 'bitbucket', - 'GitLab.com' => 'gitlab', - 'Gitorious.org' => 'gitorious', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', - 'Repo by URL' => 'git', + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', 'GitLab export' => 'gitlab_project' } end diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index b4493bf44d2..01c96a6fe96 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -4,6 +4,17 @@ module Gitlab class RackMiddleware CONTROLLER_KEY = 'action_controller.instance' ENDPOINT_KEY = 'api.endpoint' + CONTENT_TYPES = { + 'text/html' => :html, + 'text/plain' => :txt, + 'application/json' => :json, + 'text/js' => :js, + 'application/atom+xml' => :atom, + 'image/png' => :png, + 'image/jpeg' => :jpeg, + 'image/gif' => :gif, + 'image/svg+xml' => :svg + } def initialize(app) @app = app @@ -46,8 +57,15 @@ module Gitlab end def tag_controller(trans, env) - controller = env[CONTROLLER_KEY] - trans.action = "#{controller.class.name}##{controller.action_name}" + controller = env[CONTROLLER_KEY] + action = "#{controller.class.name}##{controller.action_name}" + suffix = CONTENT_TYPES[controller.content_type] + + if suffix && suffix != :html + action += ".#{suffix}" + end + + trans.action = action end def tag_endpoint(trans, env) diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb index 56608b1b276..5d2d7d0026c 100644 --- a/lib/gitlab/middleware/rails_queue_duration.rb +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -11,7 +11,7 @@ module Gitlab def call(env) trans = Gitlab::Metrics.current_transaction - proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence + proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence if trans && proxy_start # Time in milliseconds since gitlab-workhorse started the request trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index fe65c246101..99d0c28e749 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -22,6 +22,8 @@ module Gitlab note_url when WikiPage wiki_page_url + when ProjectSnippet + project_snippet_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index d13fe0ef8a9..e59ead5d76c 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -7,7 +7,7 @@ module Gitlab # @param cmd [Array<String>] # @return [Boolean] def system_silent(cmd) - Popen::popen(cmd).last.zero? + Popen.popen(cmd).last.zero? end def force_utf8(str) diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb index d5f0b289b5b..8be662974a0 100644 --- a/spec/controllers/admin/impersonations_controller_spec.rb +++ b/spec/controllers/admin/impersonations_controller_spec.rb @@ -77,6 +77,8 @@ describe Admin::ImpersonationsController do context "when the impersonator is not blocked" do it "redirects to the impersonated user's page" do + expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original + delete :destroy expect(response).to redirect_to(admin_user_path(user)) diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 44128a43362..a121cb2fc97 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -237,6 +237,56 @@ describe AutocompleteController do end end + context 'authorized projects apply limit' do + before do + authorized_project2 = create(:project) + authorized_project3 = create(:project) + + authorized_project.team << [user, :master] + authorized_project2.team << [user, :master] + authorized_project3.team << [user, :master] + + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + end + + describe 'GET #projects with project ID' do + before do + get(:projects, project_id: project.id) + end + + let(:body) { JSON.parse(response.body) } + + it do + expect(body).to be_kind_of(Array) + expect(body.size).to eq 3 # Of a total of 4 + end + end + end + + context 'authorized projects with offset' do + before do + authorized_project2 = create(:project) + authorized_project3 = create(:project) + + authorized_project.team << [user, :master] + authorized_project2.team << [user, :master] + authorized_project3.team << [user, :master] + end + + describe 'GET #projects with project ID and offset_id' do + before do + get(:projects, project_id: project.id, offset_id: authorized_project.id) + end + + let(:body) { JSON.parse(response.body) } + + it do + expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there + expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either + end + end + end + context 'authorized projects without admin_issue ability' do before(:each) do authorized_project.team << [user, :guest] diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb deleted file mode 100644 index 4ae2b78e11c..00000000000 --- a/spec/controllers/import/gitorious_controller_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'spec_helper' - -describe Import::GitoriousController do - include ImportSpecHelper - - let(:user) { create(:user) } - - before do - sign_in(user) - end - - describe "GET new" do - it "redirects to import endpoint on gitorious.org" do - get :new - - expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback") - end - end - - describe "GET callback" do - it "stores repo list in session" do - get :callback, repos: 'foo/bar,baz/qux' - - expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux') - end - end - - describe "GET status" do - before do - @repo = OpenStruct.new(full_name: 'asd/vim') - end - - it "assigns variables" do - @project = create(:project, import_type: 'gitorious', creator_id: user.id) - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [@repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end - end - - describe "POST create" do - before do - @repo = Gitlab::GitoriousImport::Repository.new('asd/vim') - end - - it "takes already existing namespace" do - namespace = create(:namespace, name: "asd", owner: user) - expect(Gitlab::GitoriousImport::ProjectCreator). - to receive(:new).with(@repo, namespace, user). - and_return(double(execute: true)) - stub_client(repo: @repo) - - post :create, format: :js - end - end -end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 9496636e3cc..261f35f28ed 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -39,7 +39,7 @@ describe Projects::Boards::ListsController do allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false) end - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do read_board_list user: user expect(response).to have_http_status(403) @@ -56,9 +56,9 @@ describe Projects::Boards::ListsController do end describe 'POST create' do - let(:label) { create(:label, project: project, name: 'Development') } - context 'with valid params' do + let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a successful 200 response' do create_board_list user: user, label_id: label.id @@ -73,20 +73,29 @@ describe Projects::Boards::ListsController do end context 'with invalid params' do - it 'returns an error' do - create_board_list user: user, label_id: nil + context 'when label is nil' do + it 'returns a not found 404 response' do + create_board_list user: user, label_id: nil + + expect(response).to have_http_status(404) + end + end - parsed_response = JSON.parse(response.body) + context 'when label that does not belongs to project' do + it 'returns a not found 404 response' do + label = create(:label, name: 'Development') - expect(parsed_response['label']).to contain_exactly "can't be blank" - expect(response).to have_http_status(422) + create_board_list user: user, label_id: label.id + + expect(response).to have_http_status(404) + end end end context 'with unauthorized user' do - let(:label) { create(:label, project: project, name: 'Development') } + it 'returns a forbidden 403 response' do + label = create(:label, project: project, name: 'Development') - it 'returns a successful 403 response' do create_board_list user: guest, label_id: label.id expect(response).to have_http_status(403) @@ -122,7 +131,7 @@ describe Projects::Boards::ListsController do end context 'with invalid position' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do move user: user, list: planning, position: 6 expect(response).to have_http_status(422) @@ -138,7 +147,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do move user: guest, list: planning, position: 6 expect(response).to have_http_status(403) @@ -180,7 +189,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do remove_board_list user: guest, list: planning expect(response).to have_http_status(403) @@ -213,7 +222,7 @@ describe Projects::Boards::ListsController do end context 'when board lists is not empty' do - it 'returns a unprocessable entity 422 response' do + it 'returns an unprocessable entity 422 response' do create(:list, board: board) generate_default_board_lists user: user @@ -223,7 +232,7 @@ describe Projects::Boards::ListsController do end context 'with unauthorized user' do - it 'returns a successful 403 response' do + it 'returns a forbidden 403 response' do generate_default_board_lists user: guest expect(response).to have_http_status(403) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0836b71056c..16929767ddf 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -8,13 +8,13 @@ describe Projects::IssuesController do describe "GET #index" do context 'external issue tracker' do it 'redirects to the external issue tracker' do - external = double(issues_url: 'https://example.com/issues') + external = double(project_path: 'https://example.com/project') allow(project).to receive(:external_issue_tracker).and_return(external) controller.instance_variable_set(:@project, project) get :index, namespace_id: project.namespace.path, project_id: project - expect(response).to redirect_to('https://example.com/issues') + expect(response).to redirect_to('https://example.com/project') end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c64c2b075c5..a219400d75f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.closed?).to be_truthy end + + it 'allows editing of a closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + title: 'New title' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.title).to eq 'New title' + end + + it 'does not allow to update target branch closed merge request' do + merge_request.close! + + put :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + target_branch: 'new_branch' + } + + expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } + end end end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 4fd51a23490..424ecc65759 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -14,6 +14,7 @@ FactoryGirl.define do note_events true build_events true pipeline_events true + wiki_page_events true end end end diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb index f4e5c26b519..1df972843e2 100644 --- a/spec/features/admin/admin_system_info_spec.rb +++ b/spec/features/admin/admin_system_info_spec.rb @@ -6,12 +6,49 @@ describe 'Admin System Info' do end describe 'GET /admin/system_info' do - it 'shows system info page' do - visit admin_system_info_path + let(:cpu) { double(:cpu, length: 2) } + let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) } - expect(page).to have_content 'CPU' - expect(page).to have_content 'Memory' - expect(page).to have_content 'Disks' + context 'when all info is available' do + before do + allow(Vmstat).to receive(:cpu).and_return(cpu) + allow(Vmstat).to receive(:memory).and_return(memory) + visit admin_system_info_path + end + + it 'shows system info page' do + expect(page).to have_content 'CPU 2 cores' + expect(page).to have_content 'Memory 4 GB / 16 GB' + expect(page).to have_content 'Disks' + end + end + + context 'when CPU info is not available' do + before do + allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT) + allow(Vmstat).to receive(:memory).and_return(memory) + visit admin_system_info_path + end + + it 'shows system info page with no CPU info' do + expect(page).to have_content 'CPU Unable to collect CPU info' + expect(page).to have_content 'Memory 4 GB / 16 GB' + expect(page).to have_content 'Disks' + end + end + + context 'when memory info is not available' do + before do + allow(Vmstat).to receive(:cpu).and_return(cpu) + allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT) + visit admin_system_info_path + end + + it 'shows system info page with no CPU info' do + expect(page).to have_content 'CPU 2 cores' + expect(page).to have_content 'Memory Unable to collect memory info' + expect(page).to have_content 'Disks' + end end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 8910c50c294..55e5dc15428 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do end end + it 'search backlog list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue1.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + + it 'search done list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue8.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + end + + it 'search list' do + page.within('#js-boards-seach') do + find('.form-control').set(issue5.title) + end + + wait_for_vue_resource + + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + end + it 'allows user to delete board' do page.within(find('.board:nth-child(2)')) do find('.board-delete').click @@ -162,32 +201,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'is searchable' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - wait_for_vue_resource(spinner: false) - - expect(page).to have_selector('.card', count: 1) - end - end - - it 'clears search' do - page.within(find('.board', match: :first)) do - find('.form-control').set issue1.title - - expect(page).to have_selector('.card', count: 1) - - find('.board-search-clear-btn').click - end - - wait_for_vue_resource - - page.within(find('.board', match: :first)) do - expect(page).to have_selector('.card', count: 6) - end - end - it 'moves issue from backlog into list' do drag_to(list_to_index: 1) @@ -572,6 +585,18 @@ describe 'Issue Boards', feature: true, js: true do end end + context 'keyboard shortcuts' do + before do + visit namespace_project_board_path(project.namespace, project) + wait_for_vue_resource + end + + it 'allows user to use keyboard shortcuts' do + find('.boards-list').native.send_keys('i') + expect(page).to have_content('New Issue') + end + end + context 'signed out user' do before do logout diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 6eb04cf74c5..79cc50bc18e 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do describe 'Click award emoji from issue#show' do let!(:issue) do create(:issue, - author: @user, assignee: @user, project: project) end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index e528aff4d41..fb0c4704285 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do context "when there is a referenced merge request" do let(:note) do create(:note, :on_issue, :system, project: project, - note: "mentioned in !#{referenced_mr.iid}") + note: "Mentioned in !#{referenced_mr.iid}") end let(:referenced_mr) do create(:merge_request, :simple, source_project: project, target_project: project, diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index cb445e22af0..d744d0eb6af 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -122,6 +122,17 @@ describe 'Issues', feature: true do expect(page).to have_content date.to_s(:medium) end end + + it 'warns about version conflict' do + issue.update(title: "New title") + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + click_button 'Save changes' + + expect(page).to have_content 'Someone edited the issue the same time you did' + end end end @@ -525,7 +536,7 @@ describe 'Issues', feature: true do end end - describe 'new issue by email' do + xdescribe 'new issue by email' do shared_examples 'show the email in the modal' do before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 930c36ade2b..759edf8ec80 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -43,7 +43,8 @@ feature 'Merge request conflict resolution', js: true, feature: true do 'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-binary-file' => 'when the conflicts contain a binary file', 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers', - 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another' + 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', + 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', } UNRESOLVABLE_CONFLICTS.each do |source_branch, description| diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb new file mode 100644 index 00000000000..06fad1007e8 --- /dev/null +++ b/spec/features/merge_requests/diff_notes_spec.rb @@ -0,0 +1,238 @@ +require 'spec_helper' + +feature 'Diff notes', js: true, feature: true do + include WaitForAjax + + before do + login_as :admin + @merge_request = create(:merge_request) + @project = @merge_request.source_project + end + + context 'merge request diffs' do + let(:comment_button_class) { '.add-diff-note' } + let(:notes_holder_input_class) { 'js-temp-notes-holder' } + let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } + let(:test_note_comment) { 'this is a test note!' } + + context 'when hovering over a parallel view diff file' do + before(:each) do + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel') + end + + context 'with an old line on the left and no line on the right' do + it 'should allow commenting on the left side' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left') + end + + it 'should not allow commenting on the right side' do + should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right') + end + end + + context 'with no line on the left and a new line on the right' do + it 'should not allow commenting on the left side' do + should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left') + end + + it 'should allow commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right') + end + end + + context 'with an old line on the left and a new line on the right' do + it 'should allow commenting on the left side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left') + end + + it 'should allow commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right') + end + end + + context 'with an unchanged line on the left and an unchanged line on the right' do + it 'should allow commenting on the left side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left') + end + + it 'should allow commenting on the right side' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right') + end + end + + context 'with a match line' do + it 'should not allow commenting on the left side' do + should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left') + end + + it 'should not allow commenting on the right side' do + should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right') + end + end + + context 'with an unfolded line' do + before(:each) do + find('.js-unfold', match: :first).click + wait_for_ajax + end + + # The first `.js-unfold` unfolds upwards, therefore the first + # `.line_holder` will be an unfolded line. + let(:line_holder) { first('.line_holder[id="1"]') } + + it 'should not allow commenting on the left side' do + should_not_allow_commenting(line_holder, 'left') + end + + it 'should not allow commenting on the right side' do + should_not_allow_commenting(line_holder, 'right') + end + end + end + + context 'when hovering over an inline view diff file' do + before do + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + end + + context 'with a new line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'should not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + + context 'with an unfolded line' do + before(:each) do + find('.js-unfold', match: :first).click + wait_for_ajax + end + + # The first `.js-unfold` unfolds upwards, therefore the first + # `.line_holder` will be an unfolded line. + let(:line_holder) { first('.line_holder[id="1"]') } + + it 'should not allow commenting' do + should_not_allow_commenting line_holder + end + end + + context 'when hovering over a diff discussion' do + before do + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + end + + it 'should not allow commenting' do + should_not_allow_commenting(find('.line_holder', match: :first)) + end + end + end + + context 'when the MR only supports legacy diff notes' do + before do + @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline') + end + + context 'with a new line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + + context 'with an old line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) + end + end + + context 'with an unchanged line' do + it 'should allow commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) + end + end + + context 'with a match line' do + it 'should not allow commenting' do + should_not_allow_commenting(find('.match', match: :first)) + end + end + end + + def should_allow_commenting(line_holder, diff_side = nil) + line = get_line_components(line_holder, diff_side) + line[:content].hover + expect(line[:num]).to have_css comment_button_class + + comment_on_line(line_holder, line) + + assert_comment_persistence(line_holder) + end + + def should_not_allow_commenting(line_holder, diff_side = nil) + line = get_line_components(line_holder, diff_side) + line[:content].hover + expect(line[:num]).not_to have_css comment_button_class + end + + def get_line_components(line_holder, diff_side = nil) + if diff_side.nil? + get_inline_line_components(line_holder) + else + get_parallel_line_components(line_holder, diff_side) + end + end + + def get_inline_line_components(line_holder) + { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) } + end + + def get_parallel_line_components(line_holder, diff_side = nil) + side_index = diff_side == 'left' ? 0 : 1 + # Wait for `.line_content` + line_holder.find('.line_content', match: :first) + # Wait for `.diff-line-num` + line_holder.find('.diff-line-num', match: :first) + { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] } + end + + def comment_on_line(line_holder, line) + line[:num].find(comment_button_class).trigger 'click' + line_holder.find(:xpath, notes_holder_input_xpath) + + notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath) + expect(notes_holder_input[:class]).to include(notes_holder_input_class) + + notes_holder_input.fill_in 'note[note]', with: test_note_comment + click_button 'Comment' + wait_for_ajax + end + + def assert_comment_persistence(line_holder) + expect(line_holder).to have_xpath notes_holder_input_xpath + + notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath) + expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class) + expect(notes_holder_saved).to have_content test_note_comment + end + end +end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index 4109e78f560..c77e719c5df 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -17,5 +17,16 @@ feature 'Edit Merge Request', feature: true do it 'has class js-quick-submit in form' do expect(page).to have_selector('.js-quick-submit') end + + it 'warns about version conflict' do + merge_request.update(title: "New title") + + fill_in 'merge_request_title', with: 'bug 345' + fill_in 'merge_request_description', with: 'bug description' + + click_button 'Save changes' + + expect(page).to have_content 'Someone edited the merge request the same time you did' + end end end diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb new file mode 100644 index 00000000000..577c910f11b --- /dev/null +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +feature 'Merge Request versions', js: true, feature: true do + before do + login_as :admin + merge_request = create(:merge_request, importing: true) + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project = merge_request.source_project + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'show the latest version of the diff' do + page.within '.mr-version-switch' do + expect(page).to have_content 'Version: latest' + end + + expect(page).to have_content '8 changed files' + end + + describe 'switch between versions' do + before do + page.within '.mr-version-switch' do + find('.btn-link').click + click_link '6f6d7e7e' + end + end + + it 'should show older version' do + page.within '.mr-version-switch' do + expect(page).to have_content 'Version: 6f6d7e7e' + end + + expect(page).to have_content '5 changed files' + end + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 7a9edbbe339..f1c522155d3 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -141,7 +141,7 @@ describe 'Comments', feature: true do let(:project2) { create(:project, :private) } let(:issue) { create(:issue, project: project2) } let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') } - let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") } + let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") } it 'shows the system note' do login_as :admin diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index af86d3c338a..5972e7f31c2 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -4,12 +4,6 @@ feature 'test coverage badge' do given!(:user) { create(:user) } given!(:project) { create(:project, :private) } - given!(:pipeline) do - create(:ci_pipeline, project: project, - ref: 'master', - sha: project.commit.id) - end - context 'when user has access to view badge' do background do project.team << [user, :developer] @@ -17,8 +11,10 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge image for pipeline' do - create_job(coverage: 100, name: 'test:1') - create_job(coverage: 90, name: 'test:2') + create_pipeline do |pipeline| + create_build(pipeline, coverage: 100, name: 'test:1') + create_build(pipeline, coverage: 90, name: 'test:2') + end show_test_coverage_badge @@ -26,9 +22,11 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge for specific job' do - create_job(coverage: 50, name: 'test:1') - create_job(coverage: 50, name: 'test:2') - create_job(coverage: 85, name: 'coverage') + create_pipeline do |pipeline| + create_build(pipeline, coverage: 50, name: 'test:1') + create_build(pipeline, coverage: 50, name: 'test:2') + create_build(pipeline, coverage: 85, name: 'coverage') + end show_test_coverage_badge(job: 'coverage') @@ -36,7 +34,9 @@ feature 'test coverage badge' do end scenario 'user requests coverage badge for pipeline without coverage' do - create_job(coverage: nil, name: 'test') + create_pipeline do |pipeline| + create_build(pipeline, coverage: nil, name: 'test') + end show_test_coverage_badge @@ -54,10 +54,19 @@ feature 'test coverage badge' do end end - def create_job(coverage:, name:) - create(:ci_build, name: name, - coverage: coverage, - pipeline: pipeline) + def create_pipeline + opts = { project: project, ref: 'master', sha: project.commit.id } + + create(:ci_pipeline, opts).tap do |pipeline| + yield pipeline + pipeline.build_updated + end + end + + def create_build(pipeline, coverage:, name:) + opts = { pipeline: pipeline, coverage: coverage, name: name } + + create(:ci_build, :success, opts) end def show_test_coverage_badge(job: nil) diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb new file mode 100644 index 00000000000..63878c55421 --- /dev/null +++ b/spec/features/projects/branches/delete_spec.rb @@ -0,0 +1,24 @@ +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/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb new file mode 100644 index 00000000000..04058300570 --- /dev/null +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in branches page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('binary-encoding').sha, + ref: 'binary-encoding', # make sure the branch is in the 1st page! + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking branches' do + context 'with artifacts' do + before do + visit namespace_project_branches_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 79abba21854..1b14945bf0a 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -20,7 +20,7 @@ describe 'Branches', feature: true do describe 'Find branches' do it 'shows filtered branches', js: true do - visit namespace_project_branches_path(project.namespace, project, project.id) + visit namespace_project_branches_path(project.namespace, project) fill_in 'branch-search', with: 'fix' find('#branch-search').native.send_keys(:enter) diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb index 0cfeb2e57d8..0cfeb2e57d8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb index 1b4ff6b6f1b..e45e3a36d01 100644 --- a/spec/features/projects/commits/cherry_pick_spec.rb +++ b/spec/features/projects/commits/cherry_pick_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +include WaitForAjax describe 'Cherry-pick Commits' do let(:project) { create(:project) } @@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do before do login_as :user project.team << [@user, :master] - visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 }) + visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) end context "I cherry-pick a commit" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click expect(page).not_to have_content('v1.0.0') # Only branches, not tags page.within('#modal-cherry-pick-commit') do @@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a merge commit" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do uncheck 'create_merge_request' @@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a commit that was previously cherry-picked" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do uncheck 'create_merge_request' @@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do context "I cherry-pick a commit in a new merge request" do it do - visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) find("a[href='#modal-cherry-pick-commit']").click page.within('#modal-cherry-pick-commit') do click_button 'Cherry-pick' @@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.') end end + + context "I cherry-pick a commit from a different branch", js: true do + it do + find('.commit-action-buttons a.dropdown-toggle').click + find(:css, "a[href='#modal-cherry-pick-commit']").click + + page.within('#modal-cherry-pick-commit') do + click_button 'master' + end + + wait_for_ajax + + page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do + click_link 'feature' + end + + page.within('#modal-cherry-pick-commit') do + uncheck 'create_merge_request' + click_button 'Cherry-pick' + end + + expect(page).to have_content('The commit has been successfully cherry-picked.') + end + end end diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb new file mode 100644 index 00000000000..be5cebcd7c9 --- /dev/null +++ b/spec/features/projects/files/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in files tree', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when files tree' do + context 'with artifacts' do + before do + visit namespace_project_tree_path( + project.namespace, project, project.default_branch) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb new file mode 100644 index 00000000000..1a71a03fbd9 --- /dev/null +++ b/spec/features/projects/group_links_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature 'Project group links', feature: true, js: true do + include Select2Helper + + let(:master) { create(:user) } + let(:project) { create(:project) } + let!(:group) { create(:group) } + + background do + project.team << [master, :master] + login_as(master) + end + + context 'setting an expiration date for a group link' do + before do + visit namespace_project_group_links_path(project.namespace, project) + + select2 group.id, from: '#link_group_id' + fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') + page.find('body').click + click_on 'Share' + end + + it 'shows the expiration time with a warning class' do + page.within('.enabled-groups') do + expect(page).to have_content('expires in 4 days') + expect(page).to have_selector('.text-warning') + end + end + end +end diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb new file mode 100644 index 00000000000..b26c0ea7a14 --- /dev/null +++ b/spec/features/projects/main/download_buttons_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Download buttons in project main page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking project main page' do + context 'with artifacts' do + before do + visit namespace_project_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb new file mode 100644 index 00000000000..430c384ac2e --- /dev/null +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do + include Select2Helper + include ActiveSupport::Testing::TimeHelpers + + let(:master) { create(:user) } + let(:project) { create(:project) } + let!(:new_member) { create(:user) } + + background do + project.team << [master, :master] + login_as(master) + end + + scenario 'expiration date is displayed in the members list' do + travel_to Time.zone.parse('2016-08-06 08:00') do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.users-project-form' do + select2(new_member.id, from: '#user_ids', multiple: true) + fill_in 'expires_at', with: '2016-08-10' + click_on 'Add users to project' + end + + page.within '.project_member:first-child' do + expect(page).to have_content('Expires in 4 days') + end + end + end + + scenario 'change expiration date' do + travel_to Time.zone.parse('2016-08-06 08:00') do + project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.project_member:first-child' do + click_on 'Edit' + fill_in 'Access expiration date', with: '2016-08-09' + click_on 'Save' + expect(page).to have_content('Expires in 3 days') + end + end + end +end diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb new file mode 100644 index 00000000000..6e0022c179f --- /dev/null +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Download buttons in tags page', feature: true do + given(:user) { create(:user) } + given(:role) { :developer } + given(:status) { 'success' } + given(:tag) { 'v1.0.0' } + given(:project) { create(:project) } + + given(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit(tag).sha, + ref: tag, + status: status) + end + + given!(:build) do + create(:ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build') + end + + background do + login_as(user) + project.team << [user, role] + end + + describe 'when checking tags' do + context 'with artifacts' do + before do + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'shows download artifacts button' do + expect(page).to have_link "Download '#{build.name}'" + end + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 1b14c66fe28..e00d85904d5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -115,6 +115,35 @@ feature 'Project', feature: true do end end + describe 'tree view (default view is set to Files)' do + let(:user) { create(:user, project_view: 'files') } + let(:project) { create(:forked_project_with_submodules) } + + before do + project.team << [user, :master] + login_as user + visit namespace_project_path(project.namespace, project) + end + + it 'has working links to files' do + click_link('PROCESS.md') + + expect(page.status_code).to eq(200) + end + + it 'has working links to directories' do + click_link('encoding') + + expect(page.status_code).to eq(200) + end + + it 'has working links to submodules' do + click_link('645f6c4c') + + expect(page.status_code).to eq(200) + end + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb new file mode 100644 index 00000000000..395c61a4743 --- /dev/null +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -0,0 +1,71 @@ +RSpec.shared_examples "protected branches > access control > CE" do + ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can push to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + allowed_to_push_button = find(".js-allowed-to-push") + + unless allowed_to_push_button.text == access_type_name + allowed_to_push_button.click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) + end + + it "allows updating protected branches so that #{access_type_name} can push to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".js-allowed-to-push").click + within('.js-allowed-to-push-container') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) + end + end + + ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can merge to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + allowed_to_merge_button = find(".js-allowed-to-merge") + + unless allowed_to_merge_button.text == access_type_name + allowed_to_merge_button.click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) + end + + it "allows updating protected branches so that #{access_type_name} can merge to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".js-allowed-to-merge").click + within('.js-allowed-to-merge-container') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) + end + end +end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index a0ee6cab7ec..1a3f7b970f6 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f } feature 'Projected Branches', feature: true, js: true do include WaitForAjax @@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do end describe "access control" do - ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| - it "allows creating protected branches that #{access_type_name} can push to" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - within('.new_protected_branch') do - allowed_to_push_button = find(".js-allowed-to-push") - - unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } - end - end - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) - end - - it "allows updating protected branches so that #{access_type_name} can push to them" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - - within(".protected-branches-list") do - find(".js-allowed-to-push").click - within('.js-allowed-to-push-container') { click_on access_type_name } - end - - wait_for_ajax - expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) - end - end - - ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| - it "allows creating protected branches that #{access_type_name} can merge to" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - within('.new_protected_branch') do - allowed_to_merge_button = find(".js-allowed-to-merge") - - unless allowed_to_merge_button.text == access_type_name - allowed_to_merge_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } - end - end - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) - end - - it "allows updating protected branches so that #{access_type_name} can merge to them" do - visit namespace_project_protected_branches_path(project.namespace, project) - set_protected_branch_name('master') - click_on "Protect" - - expect(ProtectedBranch.count).to eq(1) - - within(".protected-branches-list") do - find(".js-allowed-to-merge").click - within('.js-allowed-to-merge-container') { click_on access_type_name } - end - - wait_for_ajax - expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) - end - end + include_examples "protected branches > access control > CE" end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index b7a25d80fec..dcd3a2f17b0 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -71,6 +71,16 @@ describe "Search", feature: true do end describe 'Right header search field', feature: true do + it 'allows enter key to search', js: true do + visit namespace_project_path(project.namespace, project) + fill_in 'search', with: 'gitlab' + find('#search').native.send_keys(:enter) + + page.within '.title' do + expect(page).to have_content 'Search' + end + end + describe 'Search in project page' do before do visit namespace_project_path(project.namespace, project) diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 788581a26cb..40f773956d1 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do it { is_expected.to be_allowed_for :visitor } end + describe "GET /koding" do + subject { koding_path } + + context 'with Koding enabled' do + before do + stub_application_setting(koding_enabled?: true) + end + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + describe "GET /projects/new" do it { expect(new_project_path).to be_allowed_for :admin } it { expect(new_project_path).to be_allowed_for :user } diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 6ed279ef9be..abb27c90e0a 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:singleIncompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [ ] Incomplete entry 1 + MARKDOWN + end + + let(:singleCompleteMarkdown) do + <<-MARKDOWN.strip_heredoc + This is a task list: + + - [x] Incomplete entry 1 + MARKDOWN + end + before do Warden.test_mode! @@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do end describe 'for Issues' do - let!(:issue) { create(:issue, description: markdown, author: user, project: project) } + describe 'multiple tasks' do + let!(:issue) { create(:issue, description: markdown, author: user, project: project) } - it 'renders' do - visit_issue(project, issue) + it 'renders' do + visit_issue(project, issue) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + container = '.detail-page-description .description.js-task-list-container' - it 'contains the required selectors' do - visit_issue(project, issue) + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - container = '.detail-page-description .description.js-task-list-container' + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single incomplete task' do + let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } - logout(:user) + it 'renders' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on Issues#index' do - visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } + + it 'renders' do + visit_issue(project, issue) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on Issues#index' do + visit namespace_project_issues_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end describe 'for Notes' do let!(:issue) { create(:issue, author: user, project: project) } - let!(:note) do - create(:note, note: markdown, noteable: issue, - project: project, author: user) + describe 'multiple tasks' do + let!(:note) do + create(:note, note: markdown, noteable: issue, + project: project, author: user) + end + + it 'renders for note body' do + visit_issue(project, issue) + + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 6) + expect(page).to have_selector('.note ul input[checked]', count: 2) + end + + it 'contains the required selectors' do + visit_issue(project, issue) + + expect(page).to have_selector('.note .js-task-list-container') + expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') + expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + end + + it 'is only editable by author' do + visit_issue(project, issue) + expect(page).to have_selector('.js-task-list-container') + + logout(:user) + + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end end - it 'renders for note body' do - visit_issue(project, issue) - - expect(page).to have_selector('.note ul.task-list', count: 1) - expect(page).to have_selector('.note li.task-list-item', count: 6) - expect(page).to have_selector('.note ul input[checked]', count: 2) - end + describe 'single incomplete task' do + let!(:note) do + create(:note, note: singleIncompleteMarkdown, noteable: issue, + project: project, author: user) + end - it 'contains the required selectors' do - visit_issue(project, issue) + it 'renders for note body' do + visit_issue(project, issue) - expect(page).to have_selector('.note .js-task-list-container') - expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox') - expect(page).to have_selector('.note .js-task-list-container .js-task-list-field') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 0) + end end - it 'is only editable by author' do - visit_issue(project, issue) - expect(page).to have_selector('.js-task-list-container') + describe 'single complete task' do + let!(:note) do + create(:note, note: singleCompleteMarkdown, noteable: issue, + project: project, author: user) + end - logout(:user) + it 'renders for note body' do + visit_issue(project, issue) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + expect(page).to have_selector('.note ul.task-list', count: 1) + expect(page).to have_selector('.note li.task-list-item', count: 1) + expect(page).to have_selector('.note ul input[checked]', count: 1) + end end end @@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do visit namespace_project_merge_request_path(project.namespace, project, merge) end - let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } + describe 'multiple tasks' do + let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } - it 'renders for description' do - visit_merge_request(project, merge) + it 'renders for description' do + visit_merge_request(project, merge) - expect(page).to have_selector('ul.task-list', count: 1) - expect(page).to have_selector('li.task-list-item', count: 6) - expect(page).to have_selector('ul input[checked]', count: 2) - end + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 6) + expect(page).to have_selector('ul input[checked]', count: 2) + end - it 'contains the required selectors' do - visit_merge_request(project, merge) + it 'contains the required selectors' do + visit_merge_request(project, merge) - container = '.detail-page-description .description.js-task-list-container' + container = '.detail-page-description .description.js-task-list-container' - expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") - expect(page).to have_selector("#{container} .js-task-list-field") - expect(page).to have_selector('form.js-issuable-update') - expect(page).to have_selector('a.btn-close') - end + expect(page).to have_selector(container) + expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .js-task-list-field") + expect(page).to have_selector('form.js-issuable-update') + expect(page).to have_selector('a.btn-close') + end - it 'is only editable by author' do - visit_merge_request(project, merge) - expect(page).to have_selector('.js-task-list-container') + it 'is only editable by author' do + visit_merge_request(project, merge) + expect(page).to have_selector('.js-task-list-container') - logout(:user) + logout(:user) - login_as(user2) - visit current_path - expect(page).not_to have_selector('.js-task-list-container') + login_as(user2) + visit current_path + expect(page).not_to have_selector('.js-task-list-container') + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("2 of 6 tasks completed") + end + end + + describe 'single incomplete task' do + let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 0) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("0 of 1 task completed") + end end - it 'provides a summary on MergeRequests#index' do - visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_content("6 tasks (2 completed, 4 remaining)") + describe 'single complete task' do + let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) } + + it 'renders for description' do + visit_merge_request(project, merge) + + expect(page).to have_selector('ul.task-list', count: 1) + expect(page).to have_selector('li.task-list-item', count: 1) + expect(page).to have_selector('ul input[checked]', count: 1) + end + + it 'provides a summary on MergeRequests#index' do + visit namespace_project_merge_requests_path(project.namespace, project) + expect(page).to have_content("1 of 1 task completed") + end end end end diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb new file mode 100644 index 00000000000..e74a51acede --- /dev/null +++ b/spec/features/todos/todos_sorting_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe "Dashboard > User sorts todos", feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + 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 + + 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" + + 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 diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 0342f4f1d97..32544f3f538 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_content("You're all done!") end end + + context 'todo is stale on the page' do + before do + todos = TodosFinder.new(user, state: :pending).execute + TodoService.new.mark_todos_as_done(todos, user) + end + + describe 'deleting the todo' do + before do + first('.done-todo').click + end + + it 'is removed from the list' do + expect(page).not_to have_selector('.todos-list .todo') + end + + it 'shows "All done" message' do + expect(page).to have_content("You're all done!") + end + end + end end context 'User has Todos with labels spanning multiple projects' do diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb index 4f3304f7b6d..fdce4e714ff 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/move_to_project_finder_spec.rb @@ -51,6 +51,28 @@ describe MoveToProjectFinder do expect(subject.execute(project).to_a).to eq([other_reporter_project]) end + + it 'returns a page of projects ordered by id in descending order' do + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + + reporter_project.team << [user, :reporter] + developer_project.team << [user, :developer] + master_project.team << [user, :master] + + expect(subject.execute(project).to_a).to eq([master_project, developer_project]) + end + + it 'returns projects after the given offset id' do + stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + + reporter_project.team << [user, :reporter] + developer_project.team << [user, :developer] + master_project.team << [user, :master] + + expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project]) + expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) + expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty + end end context 'search' do diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb new file mode 100644 index 00000000000..f7e7e733cf7 --- /dev/null +++ b/spec/finders/todos_finder_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe TodosFinder do + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:finder) { described_class } + + before { project.team << [user, :developer] } + + describe '#sort' do + context 'by date' do + let!(:todo1) { create(:todo, user: user, project: project) } + let!(:todo2) { create(:todo, user: user, project: project) } + let!(:todo3) { create(:todo, user: user, project: project) } + + it 'sorts with oldest created first' do + todos = finder.new(user, { sort: 'id_asc' }).execute + + expect(todos.first).to eq(todo1) + expect(todos.second).to eq(todo2) + expect(todos.third).to eq(todo3) + end + + it 'sorts with newest created first' do + todos = finder.new(user, { sort: 'id_desc' }).execute + + expect(todos.first).to eq(todo3) + expect(todos.second).to eq(todo2) + expect(todos.third).to eq(todo1) + end + end + + it "sorts by priority" do + label_1 = create(:label, title: 'label_1', project: project, priority: 1) + label_2 = create(:label, title: 'label_2', project: project, priority: 2) + label_3 = create(:label, title: 'label_3', project: project, priority: 3) + + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + merge_request_1 = create(:merge_request, source_project: project) + + merge_request_1.labels << label_1 + + # Covers the case where Todo has more than one label + issue_3.labels << label_1 + issue_3.labels << label_3 + + issue_2.labels << label_3 + issue_1.labels << label_2 + + todo_1 = create(:todo, user: user, project: project, target: issue_4) + todo_2 = create(:todo, user: user, project: project, target: issue_2) + todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago) + todo_4 = create(:todo, user: user, project: project, target: issue_1) + todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + + todos = finder.new(user, { sort: 'priority' }).execute + + expect(todos.first).to eq(todo_3) + expect(todos.second).to eq(todo_5) + expect(todos.third).to eq(todo_4) + expect(todos.fourth).to eq(todo_2) + expect(todos.fifth).to eq(todo_1) + end + end + end +end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 299e4675d6f..532ebb9640e 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -10,23 +10,31 @@ "title": { "type": "string" }, "confidential": { "type": "boolean" }, "labels": { - "type": ["array"], - "required": [ - "id", - "color", - "description", - "title", - "priority" - ], - "properties": { - "id": { "type": "integer" }, - "color": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "color", + "description", + "title", + "priority" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "text_color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "title": { "type": "string" }, + "priority": { "type": ["integer", "null"] } }, - "description": { "type": ["string", "null"] }, - "title": { "type": "string" }, - "priority": { "type": ["integer", "null"] } + "additionalProperties": false } }, "assignee": { diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb new file mode 100644 index 00000000000..2dd2eab0524 --- /dev/null +++ b/spec/helpers/issuables_helper_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe IssuablesHelper do + let(:label) { build_stubbed(:label) } + let(:label2) { build_stubbed(:label) } + + context 'label tooltip' do + it 'returns label text' do + expect(issuable_labels_tooltip([label])).to eq(label.title) + end + + it 'returns label text' do + expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") + end + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 604204cca0a..284b58d8d5c 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -136,4 +136,42 @@ describe ProjectsHelper do expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git') end end + + describe '#last_push_event' do + let(:user) { double(:user, fork_of: nil) } + let(:project) { double(:project, id: 1) } + + before do + allow(helper).to receive(:current_user).and_return(user) + helper.instance_variable_set(:@project, project) + end + + context 'when there is no current_user' do + let(:user) { nil } + + it 'returns nil' do + expect(helper.last_push_event).to eq(nil) + end + end + + it 'returns recent push on the current project' do + event = double(:event) + expect(user).to receive(:recent_push).with([project.id]).and_return(event) + + expect(helper.last_push_event).to eq(event) + end + + context 'when current user has a fork of the current project' do + let(:fork) { double(:fork, id: 2) } + + it 'returns recent push considering fork events' do + expect(user).to receive(:fork_of).with(project).and_return(fork) + + event_on_fork = double(:event) + expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork) + + expect(helper.last_push_event).to eq(event_on_fork) + end + end + end end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index bf3ed5c094c..21f35585367 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -19,16 +19,16 @@ describe TimeHelper do describe "#duration_in_numbers" do it "returns minutes and seconds" do - duration_in_numbers = { - [100, 0] => "01:40", - [121, 0] => "02:01", - [3721, 0] => "01:02:01", - [0, 0] => "00:00", - [nil, Time.now.to_i - 42] => "00:42" + durations_and_expectations = { + 100 => "01:40", + 121 => "02:01", + 3721 => "01:02:01", + 0 => "00:00", + 42 => "00:42" } - duration_in_numbers.each do |interval, expectation| - expect(duration_in_numbers(*interval)).to eq(expectation) + durations_and_expectations.each do |duration, expectation| + expect(duration_in_numbers(duration)).to eq(expectation) end end end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index fa32d0d7da5..c1c12b57b53 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -11,7 +11,7 @@ /*= require ./fixtures/emoji_menu */ (function() { - var awardsHandler, lazyAssert; + var awardsHandler, lazyAssert, urlRoot; awardsHandler = null; @@ -27,6 +27,7 @@ }; gon.award_menu_url = '/emojis'; + urlRoot = gon.relative_url_root; lazyAssert = function(done, assertFn) { return setTimeout(function() { @@ -45,9 +46,14 @@ return cb(); }; })(this)); - return spyOn(jQuery, 'get').and.callFake(function(req, cb) { + spyOn(jQuery, 'get').and.callFake(function(req, cb) { return cb(window.emojiMenu); }); + spyOn(jQuery, 'cookie'); + }); + afterEach(function() { + // restore original url root value + gon.relative_url_root = urlRoot; }); describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { @@ -189,6 +195,28 @@ return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); }); }); + describe('::addEmojiToFrequentlyUsedList', function() { + it('should set a cookie with the correct default path', function() { + gon.relative_url_root = ''; + awardsHandler.addEmojiToFrequentlyUsedList('sunglasses'); + expect(jQuery.cookie) + .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', { + path: '/', + expires: 365 + }) + ; + }); + it('should set a cookie with the correct custom root path', function() { + gon.relative_url_root = '/gitlab/subdir'; + awardsHandler.addEmojiToFrequentlyUsedList('alien'); + expect(jQuery.cookie) + .toHaveBeenCalledWith('frequently_used_emojis', 'alien', { + path: '/gitlab/subdir', + expires: 365 + }) + ; + }); + }); describe('search', function() { return it('should filter the emoji', function() { $('.js-add-award').eq(0).click(); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index c206b794442..1688b996162 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -60,15 +60,6 @@ describe('List model', () => { }, 0); }); - it('can\'t search when not backlog', () => { - expect(list.canSearch()).toBe(false); - }); - - it('can search when backlog', () => { - list.type = 'backlog'; - expect(list.canSearch()).toBe(true); - }); - it('gets issue from list', (done) => { setTimeout(() => { const issue = list.findIssue(1); diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee index 6b9617341fe..8bd113e7d86 100644 --- a/spec/javascripts/datetime_utility_spec.js.coffee +++ b/spec/javascripts/datetime_utility_spec.js.coffee @@ -29,3 +29,22 @@ describe 'Date time utils', -> it 'should return Saturday', -> day = gl.utils.getDayName(new Date('07/23/2016')) expect(day).toBe('Saturday') + + describe 'get day difference', -> + it 'should return 7', -> + firstDay = new Date('07/01/2016') + secondDay = new Date('07/08/2016') + difference = gl.utils.getDayDifference(firstDay, secondDay) + expect(difference).toBe(7) + + it 'should return 31', -> + firstDay = new Date('07/01/2016') + secondDay = new Date('08/01/2016') + difference = gl.utils.getDayDifference(firstDay, secondDay) + expect(difference).toBe(31) + + it 'should return 365', -> + firstDay = new Date('07/02/2015') + secondDay = new Date('07/01/2016') + difference = gl.utils.getDayDifference(firstDay, secondDay) + expect(difference).toBe(365)
\ No newline at end of file diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml index d55936ee4f9..1ef2e8f8624 100644 --- a/spec/javascripts/fixtures/awards_handler.html.haml +++ b/spec/javascripts/fixtures/awards_handler.html.haml @@ -39,7 +39,7 @@ %span.note-role Reporter %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} %i.fa.fa-spinner.fa-spin - %i.fa.fa-smile-o + %i.fa.fa-smile-o.link-highlight .js-task-list-container.note-body.is-task-list-enabled .note-text %p Suscipit sunt quia quisquam sed eveniet ipsam. diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml new file mode 100644 index 00000000000..a20390c08ee --- /dev/null +++ b/spec/javascripts/fixtures/gl_dropdown.html.haml @@ -0,0 +1,16 @@ +%div + .dropdown.inline + %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + Projects + %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Go to project + %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}} + %i.fa.fa-times.dropdown-menu-close-icon + .dropdown-input + %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'} + %i.fa.fa-search.dropdown-input-search + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml new file mode 100644 index 00000000000..397bdc85c67 --- /dev/null +++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml @@ -0,0 +1,16 @@ +.block.labels + .sidebar-collapsed-icon.js-sidebar-labels-tooltip + .title.hide-collapsed + %a.edit-link.pull-right{ href: "#" } + Edit + .selectbox.hide-collapsed{ style: "display: none;" } + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } } + %span.dropdown-toggle-text + Label + %i.fa.fa-chevron-down + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 84e8d0ba1e4..4919d77e5a4 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -1 +1 @@ -[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] +[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 new file mode 100644 index 00000000000..b529ea6458d --- /dev/null +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -0,0 +1,119 @@ +/*= require jquery */ +/*= require gl_dropdown */ +/*= require turbolinks */ +/*= require lib/utils/common_utils */ +/*= require lib/utils/type_utility */ + +(() => { + const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; + const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`; + + const ARROW_KEYS = { + DOWN: 40, + UP: 38, + ENTER: 13, + ESC: 27 + }; + + let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) { + i = i || 0; + if (!i) direction = direction.toUpperCase(); + $('body').trigger({ + type: 'keydown', + which: ARROW_KEYS[direction], + keyCode: ARROW_KEYS[direction] + }); + i++; + if (i <= steps) { + navigateWithKeys(direction, steps, cb, i); + } else { + cb(); + } + }; + + 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]; + this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ + selectable: true, + data: this.projectsData, + text: (project) => { + (project.name_with_namespace || project.name); + }, + id: (project) => { + project.id; + } + }); + }); + + afterEach(() => { + $('body').unbind('keydown'); + this.dropdownContainerElement.unbind('keyup'); + }); + + it('should open on click', () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + this.dropdownButtonElement.click(); + expect(this.dropdownContainerElement).toHaveClass('open'); + }); + + describe('that is open', () => { + beforeEach(() => { + this.dropdownButtonElement.click(); + }); + + it('should select a following item on DOWN keypress', () => { + 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'); + }); + }); + + it('should select a previous item on UP keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0); + navigateWithKeys('down', (this.projectsData.length - 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'); + }); + }); + }); + + it('should click the selected item on ENTER keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open') + let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0 + navigateWithKeys('down', randomIndex, () => { + spyOn(Turbolinks, 'visit').and.stub(); + navigateWithKeys('enter', null, () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + 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); + }); + }); + }); + + it('should close on ESC keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC + }); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + }); + }); + }); +})(); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 new file mode 100644 index 00000000000..840c7b6d015 --- /dev/null +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -0,0 +1,89 @@ +//= require lib/utils/type_utility +//= require jquery +//= require bootstrap +//= require gl_dropdown +//= require select2 +//= require jquery.nicescroll +//= require api +//= require create_label +//= require issuable_context +//= require users_select +//= require labels_select + +(() => { + let saveLabelCount = 0; + describe('Issue dropdown sidebar', () => { + fixture.preload('issue_sidebar_label.html'); + + beforeEach(() => { + fixture.load('issue_sidebar_label.html'); + new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); + new LabelsSelect(); + + spyOn(jQuery, 'ajax').and.callFake((req) => { + const d = $.Deferred(); + let LABELS_DATA = [] + + if (req.url === '/root/test/labels.json') { + for (let i = 0; i < 10; i++) { + LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + } + } else if (req.url === '/root/test/issues/2.json') { + let tmp = [] + for (let i = 0; i < saveLabelCount; i++) { + tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + } + LABELS_DATA = {labels: tmp}; + } + + d.resolve(LABELS_DATA); + return d.promise(); + }); + }); + + it('changes collapsed tooltip when changing labels when less than 5', (done) => { + saveLabelCount = 5; + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.dropdown-content a').length).toBe(10); + + $('.dropdow-content a').each((i, $link) => { + if (i < 5) { + $link.get(0).click(); + } + }); + + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4'); + done(); + }, 0); + }, 0); + }); + + it('changes collapsed tooltip when changing labels when more than 5', (done) => { + saveLabelCount = 6; + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.dropdown-content a').length).toBe(10); + + $('.dropdow-content a').each((i, $link) => { + if (i < 5) { + $link.get(0).click(); + } + }); + + $('.edit-link').get(0).click(); + + setTimeout(() => { + expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more'); + done(); + }, 0); + }, 0); + }); + }); +})(); + diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 68d64483d67..324f5152780 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -105,13 +105,13 @@ a3 = "a[href='" + mrsAssignedToMeLink + "']"; a4 = "a[href='" + mrsIHaveCreatedLink + "']"; expect(list.find(a1).length).toBe(1); - expect(list.find(a1).text()).toBe(' Issues assigned to me '); + expect(list.find(a1).text()).toBe('Issues assigned to me'); expect(list.find(a2).length).toBe(1); - expect(list.find(a2).text()).toBe(" Issues I've created "); + expect(list.find(a2).text()).toBe("Issues I've created"); expect(list.find(a3).length).toBe(1); - expect(list.find(a3).text()).toBe(' Merge requests assigned to me '); + expect(list.find(a3).text()).toBe('Merge requests assigned to me'); expect(list.find(a4).length).toBe(1); - return expect(list.find(a4).text()).toBe(" Merge requests I've created "); + return expect(list.find(a4).text()).toBe("Merge requests I've created"); }; describe('Search autocomplete dropdown', function() { diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 36c77206a3f..e10c1f5c547 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -30,26 +30,35 @@ describe ExtractsPath, lib: true do expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") end - context 'escaped slash character in ref' do - let(:ref) { 'improve%2Fawesome' } + context 'ref contains %20' do + let(:ref) { 'foo%20bar' } + + it 'is not converted to a space in @id' do + @project.repository.add_branch(@project.owner, 'foo%20bar', 'master') - it 'has no escape sequences in @ref or @logs_path' do assign_ref_vars - expect(@ref).to eq('improve/awesome') - expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb") + expect(@id).to start_with('foo%20bar/') end end - context 'ref contains %20' do - let(:ref) { 'foo%20bar' } + context 'path contains space' do + let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } } - it 'is not converted to a space in @id' do - @project.repository.add_branch(@project.owner, 'foo%20bar', 'master') + it 'is not converted to %20 in @path' do + assign_ref_vars + + expect(@path).to eq(params[:path]) + end + end + + context 'subclass overrides get_id' do + it 'uses ref returned by get_id' do + allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } assign_ref_vars - expect(@id).to start_with('foo%20bar/') + expect(@id).to eq(get_id) end end end diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb index 1ff49602486..ab0cce6e091 100644 --- a/spec/lib/gitlab/badge/coverage/report_spec.rb +++ b/spec/lib/gitlab/badge/coverage/report_spec.rb @@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do end end - context 'pipeline exists' do - let!(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: 'master') - end + context 'when latest successful pipeline exists' do + before do + create_pipeline do |pipeline| + create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40) + create(:ci_build, :success, pipeline: pipeline, coverage: 60) + end - context 'builds exist' do - before do - create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40) - create(:ci_build, pipeline: pipeline, coverage: 60) + create_pipeline do |pipeline| + create(:ci_build, :failed, pipeline: pipeline, coverage: 10) end + end - context 'particular job specified' do - let(:job_name) { 'first' } + context 'when particular job specified' do + let(:job_name) { 'first' } - it 'returns coverage for the particular job' do - expect(badge.status).to eq 40 - end + it 'returns coverage for the particular job' do + expect(badge.status).to eq 40 end + end - context 'particular job not specified' do - let(:job_name) { '' } + context 'when particular job not specified' do + let(:job_name) { '' } + + it 'returns arithemetic mean for the pipeline' do + expect(badge.status).to eq 50 + end + end + end - it 'returns arithemetic mean for the pipeline' do - expect(badge.status).to eq 50 - end + context 'when only failed pipeline exists' do + before do + create_pipeline do |pipeline| + create(:ci_build, :failed, pipeline: pipeline, coverage: 10) end end - context 'builds do not exist' do - it_behaves_like 'unknown coverage report' + it_behaves_like 'unknown coverage report' - context 'particular job specified' do - let(:job_name) { 'nonexistent' } + context 'particular job specified' do + let(:job_name) { 'nonexistent' } - it 'retruns nil' do - expect(badge.status).to be_nil - end + it 'retruns nil' do + expect(badge.status).to be_nil end end end @@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do context 'pipeline does not exist' do it_behaves_like 'unknown coverage report' end + + def create_pipeline + opts = { project: project, sha: project.commit.id, ref: 'master' } + + create(:ci_pipeline, opts).tap do |pipeline| + yield pipeline + pipeline.build_updated + end + end end diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb index cc44e2cc054..61e2a554419 100644 --- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Gitlab::Ci::Config::Node::HiddenJob do +describe Gitlab::Ci::Config::Node::Hidden do let(:entry) { described_class.new(config) } describe 'validations' do context 'when entry config value is correct' do - let(:config) { { image: 'ruby:2.2' } } + let(:config) { [:some, :array] } describe '#value' do it 'returns key value' do - expect(entry.value).to eq(image: 'ruby:2.2') + expect(entry.value).to eq [:some, :array] end end @@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do end context 'when entry value is not correct' do - context 'incorrect config value type' do - let(:config) { ['incorrect'] } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'hidden job config should be a hash' - end - end - end - context 'when config is empty' do let(:config) { {} } diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b8d9c70479c..ae2c88aac37 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -74,7 +74,7 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.descendants.first(2)) .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) expect(entry.descendants.last) - .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden) end end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb index 65a828accde..a1d2ca1e272 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/conflict/parser_spec.rb @@ -183,6 +183,11 @@ CONFLICT expect { parse_text('a' * 102401) }. to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end + + it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do + expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }. + to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + end end end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 10537bea008..6e8fff6f516 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do end end + describe "position for a file in the initial commit" do + let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") } + + subject do + described_class.new( + old_path: "README.md", + new_path: "README.md", + old_line: nil, + new_line: 1, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+testme") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + describe "#to_json" do let(:hash) do { diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index e1153154778..a5cc7b02936 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require_relative '../email_shared_blocks' -describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do +xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context it_behaves_like :email_shared_examples diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb new file mode 100644 index 00000000000..b7c3bc4e1a7 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Importer, lib: true do + describe '#execute' do + context 'when an error occurs' do + let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) } + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + + let(:label) do + double( + name: 'Bug', + color: 'ff0000', + url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' + ) + end + + let(:milestone) do + double( + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1' + ) + end + + let(:issue1) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347' + ) + end + + let(:issue2) do + double( + number: 1348, + milestone: nil, + state: 'open', + title: nil, + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348' + ) + end + + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' + ) + end + + before do + allow(project).to receive(:import_data).and_return(double.as_null_object) + allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label]) + allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) + allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + end + + it 'returns true' do + expect(described_class.new(project).execute).to eq true + end + + it 'does not raise an error' do + expect { described_class.new(project).execute }.not_to raise_error + end + + it 'stores error messages' do + error = { + 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 has already been taken" }, + { 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/1347", errors: "Invalid Repository. Use user/repo format." }, + { 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: "Invalid Repository. Use user/repo format." }, + { 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: :wiki, errors: "Gitlab::Shell::Error" } + ] + } + + described_class.new(project).execute + + expect(project.import_error).to eq error.to_json + end + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index aa28e360993..b667abf063d 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -27,7 +27,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do created_at: created_at, updated_at: updated_at, closed_at: nil, - merged_at: nil + merged_at: nil, + url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' } end @@ -229,4 +230,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end end + + describe '#url' do + let(:raw_data) { double(base_data) } + + it 'return raw url' do + expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' + end + end end diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb deleted file mode 100644 index 946712ca38e..00000000000 --- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe Gitlab::GitoriousImport::ProjectCreator, lib: true do - let(:user) { create(:user) } - let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } - let(:namespace){ create(:group, owner: user) } - - before do - namespace.add_owner(user) - end - - it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) - - project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user) - project = project_creator.execute - - expect(project.name).to eq("Bar Baz Qux") - expect(project.path).to eq("bar-baz-qux") - expect(project.namespace).to eq(namespace) - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) - expect(project.import_type).to eq("gitorious") - expect(project.import_source).to eq("foo/bar-baz-qux") - expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git") - end -end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index a30cb2a5e38..bcaffd27909 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do end it 'tags a transaction with the name and action of a controller' do - klass = double(:klass, name: 'TestController') + klass = double(:klass, name: 'TestController', content_type: 'text/html') controller = double(:controller, class: klass, action_name: 'show') env['action_controller.instance'] = controller @@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do middleware.call(env) end - it 'tags a transaction with the method andpath of the route in the grape endpoint' do + it 'tags a transaction with the method and path of the route in the grape endpoint' do route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") endpoint = double(:endpoint, route: route) @@ -87,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do describe '#tag_controller' do let(:transaction) { middleware.transaction_from_env(env) } + let(:content_type) { 'text/html' } - it 'tags a transaction with the name and action of a controller' do + before do klass = double(:klass, name: 'TestController') - controller = double(:controller, class: klass, action_name: 'show') + controller = double(:controller, class: klass, action_name: 'show', content_type: content_type) env['action_controller.instance'] = controller + end + it 'tags a transaction with the name and action of a controller' do middleware.tag_controller(transaction, env) expect(transaction.action).to eq('TestController#show') end + + context 'when the response content type is not :html' do + let(:content_type) { 'application/json' } + + it 'appends the mime type to the transaction action' do + middleware.tag_controller(transaction, env) + + expect(transaction.action).to eq('TestController#show.json') + end + end end describe '#tag_endpoint' do diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index fd6f684db0c..168090d5b5c 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Middleware::RailsQueueDuration do end it 'sets proxy_flight_time and calls the app when the header is present' do - env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123' + env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '123' expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float)) expect(middleware.call(env)).to eq('yay') end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index fa241867858..eae9c060c38 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -493,7 +493,12 @@ describe Notify do end def invite_to_project(project:, email:, inviter:) - ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter) + Member.add_user( + project.project_members, + 'toto@example.com', + Gitlab::Access::DEVELOPER, + current_user: inviter + ) project.project_members.invite.last end @@ -740,7 +745,12 @@ describe Notify do end def invite_to_group(group:, email:, inviter:) - GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter) + Member.add_user( + group.group_members, + 'toto@example.com', + Gitlab::Access::DEVELOPER, + current_user: inviter + ) group.group_members.invite.last end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 853f6943cef..c50ca38bdd9 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,6 +171,70 @@ describe Ability, lib: true do end end + shared_examples_for ".project_abilities" do |enable_request_store| + before do + RequestStore.begin! if enable_request_store + end + + after do + if enable_request_store + RequestStore.end! + RequestStore.clear! + end + end + + describe '.project_abilities' do + let!(:project) { create(:empty_project, :public) } + let!(:user) { create(:user) } + + it 'returns permissions for admin user' do + admin = create(:admin) + + results = described_class.project_abilities(admin, project) + + expect(results.count).to eq(68) + end + + it 'returns permissions for an owner' do + results = described_class.project_abilities(project.owner, project) + + expect(results.count).to eq(68) + end + + it 'returns permissions for a master' do + project.team << [user, :master] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(60) + end + + it 'returns permissions for a developer' do + project.team << [user, :developer] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(44) + end + + it 'returns permissions for a guest' do + project.team << [user, :guest] + + results = described_class.project_abilities(user, project) + + expect(results.count).to eq(21) + end + end + end + + describe '.project_abilities with RequestStore' do + it_behaves_like ".project_abilities", true + end + + describe '.project_abilities without RequestStore' do + it_behaves_like ".project_abilities", false + end + describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do @@ -218,4 +282,17 @@ describe Ability, lib: true do end end end + + describe '.project_disabled_features_rules' do + let(:project) { build(:project) } + + subject { described_class.project_disabled_features_rules(project) } + + context 'wiki named abilities' do + it 'disables wiki abilities if the project has no wiki' do + expect(project).to receive(:has_wiki?).and_return(false) + expect(subject).to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + end + end + end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 72688137f08..02d6263094a 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe BroadcastMessage, models: true do - include ActiveSupport::Testing::TimeHelpers - subject { create(:broadcast_message) } it { is_expected.to be_valid } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ee2c3d04984..c45c2635cf4 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -948,15 +948,17 @@ describe Ci::Build, models: true do before { build.run! } it 'returns false' do - expect(build.retryable?).to be false + expect(build).not_to be_retryable end end context 'when build is finished' do - before { build.success! } + before do + build.success! + end it 'returns true' do - expect(build.retryable?).to be true + expect(build).to be_retryable end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8137e9f8f71..598df576001 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -124,17 +124,21 @@ describe Ci::Pipeline, models: true do describe 'state machine' do let(:current) { Time.now.change(usec: 0) } - let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current } - let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current } + let(:build) { create :ci_build, name: 'build1', pipeline: pipeline } describe '#duration' do before do - build.skip - build2.skip + travel_to(current - 120) do + pipeline.run + end + + travel_to(current) do + pipeline.succeed + end end it 'matches sum of builds duration' do - expect(pipeline.reload.duration).to eq(build.duration + build2.duration) + expect(pipeline.reload.duration).to eq(120) end end @@ -191,6 +195,36 @@ describe Ci::Pipeline, models: true do end end + context 'with non-empty project' do + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha) + end + + describe '#latest?' do + context 'with latest sha' do + it 'returns true' do + expect(pipeline).to be_latest + end + end + + context 'with not latest sha' do + before do + pipeline.update( + sha: project.commit("#{project.default_branch}~1").sha) + end + + it 'returns false' do + expect(pipeline).not_to be_latest + end + end + end + end + describe '#manual_actions' do subject { pipeline.manual_actions } diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb index 8e0a2a2cbde..e118432d098 100644 --- a/spec/models/concerns/statuseable_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Statuseable do +describe HasStatus do before do @object = Object.new - @object.extend(Statuseable::ClassMethods) + @object.extend(HasStatus::ClassMethods) end describe '.status' do @@ -12,7 +12,7 @@ describe Statuseable do end subject { @object.status } - + shared_examples 'build status summary' do context 'all successful' do let(:statuses) { Array.new(2) { create(type, status: :success) } } diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2277f4e13bf..fef90d9b5cb 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -65,11 +65,21 @@ describe Member, models: true do @master_user = create(:user).tap { |u| project.team << [u, :master] } @master = project.members.find_by(user_id: @master_user.id) - ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user) + Member.add_user( + project.members, + 'toto1@example.com', + Gitlab::Access::DEVELOPER, + current_user: @master_user + ) @invited_member = project.members.invite.find_by_invite_email('toto1@example.com') accepted_invite_user = build(:user) - ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user) + Member.add_user( + project.members, + 'toto2@example.com', + Gitlab::Access::DEVELOPER, + current_user: @master_user + ) @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) } requested_user = create(:user).tap { |u| project.request_access(u) } diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 29f7396f862..e5b185dc3f6 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -1,6 +1,27 @@ require 'spec_helper' describe MergeRequestDiff, models: true do + describe 'create new record' do + subject { create(:merge_request).merge_request_diff } + + it { expect(subject).to be_valid } + it { expect(subject).to be_persisted } + it { expect(subject.commits.count).to eq(5) } + it { expect(subject.diffs.count).to eq(8) } + it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') } + end + + describe '#latest' do + let!(:mr) { create(:merge_request, :with_diffs) } + let!(:first_diff) { mr.merge_request_diff } + let!(:last_diff) { mr.create_merge_request_diff } + + it { expect(last_diff.latest?).to be_truthy } + it { expect(first_diff.latest?).to be_falsey } + end + describe '#diffs' do let(:mr) { create(:merge_request, :with_diffs) } let(:mr_diff) { mr.merge_request_diff } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 64c56d922ff..5bf3b8e609e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -9,7 +9,7 @@ describe MergeRequest, models: true do it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } - it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } + it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) } end describe 'modules' do @@ -159,7 +159,7 @@ describe MergeRequest, models: true do context 'when there are MR diffs' do it 'delegates to the MR diffs' do - merge_request.merge_request_diff = MergeRequestDiff.new + merge_request.save expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)) @@ -316,7 +316,7 @@ describe MergeRequest, models: true do end it "can be removed if the last commit is the head of the source branch" do - allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit) + allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit) expect(subject.can_remove_source_branch?(user)).to be_truthy end @@ -477,8 +477,8 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_head_sha).and_return('123abc') - expect(subject.source_project).to receive(:pipeline). - with('123abc', 'master'). + expect(subject.source_project).to receive(:pipeline_for). + with('master', '123abc'). and_return(pipeline) expect(subject.pipeline).to eq(pipeline) @@ -721,12 +721,15 @@ describe MergeRequest, models: true do let(:commit) { subject.project.commit(sample_commit.id) } - it "reloads the diff content" do - expect(subject.merge_request_diff).to receive(:reload_content) - + it "does not change existing merge request diff" do + expect(subject.merge_request_diff).not_to receive(:save_git_content) subject.reload_diff end + it "creates new merge request diff" do + expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1) + end + it "executs diff cache service" do expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) @@ -736,13 +739,15 @@ describe MergeRequest, models: true do it "updates diff note positions" do old_diff_refs = subject.diff_refs - merge_request_diff = subject.merge_request_diff - # Update merge_request_diff so that #diff_refs will return commit.diff_refs - allow(merge_request_diff).to receive(:reload_content) do - merge_request_diff.base_commit_sha = commit.parent_id - merge_request_diff.start_commit_sha = commit.parent_id - merge_request_diff.head_commit_sha = commit.sha + allow(subject).to receive(:create_merge_request_diff) do + subject.merge_request_diffs.create( + base_commit_sha: commit.parent_id, + start_commit_sha: commit.parent_id, + head_commit_sha: commit.sha + ) + + subject.merge_request_diff(true) end expect(Notes::DiffPositionUpdateService).to receive(:new).with( @@ -752,14 +757,31 @@ describe MergeRequest, models: true do new_diff_refs: commit.diff_refs, paths: note.position.paths ).and_call_original - expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) + expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) expect_any_instance_of(DiffNote).to receive(:save).once subject.reload_diff end end + describe '#branch_merge_base_commit' do + context 'source and target branch exist' do + it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + it { expect(subject.branch_merge_base_commit).to be_a(Commit) } + end + + context 'when the target branch does not exist' do + before do + subject.project.repository.raw_repository.delete_branch(subject.target_branch) + end + + it 'returns nil' do + expect(subject.branch_merge_base_commit).to be_nil + end + end + end + describe "#diff_sha_refs" do context "with diffs" do subject { create(:merge_request, :with_diffs) } @@ -890,6 +912,19 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey end + it 'returns a falsey value when the MR is marked as having conflicts, but has none' do + merge_request = create_merge_request('master') + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the MR has a missing ref after a force push' do + merge_request = create_merge_request('conflict-resolvable') + allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError) + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey + end + it 'returns a falsey value when the MR does not support new diff notes' do merge_request = create_merge_request('conflict-resolvable') merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) @@ -927,4 +962,80 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end end + + describe "#forked_source_project_missing?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the fork exists" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the source project is the same as the target project" do + let(:merge_request) { create(:merge_request, source_project: project) } + + it { expect(merge_request.forked_source_project_missing?).to be_falsey } + end + + context "when the fork does not exist" do + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns true" do + unlink_project.execute + merge_request.reload + + expect(merge_request.forked_source_project_missing?).to be_truthy + end + end + end + + describe "#closed_without_fork?" do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:user) { create(:user) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + context "when the merge request is closed" do + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false if the fork exist" do + expect(closed_merge_request.closed_without_fork?).to be_falsey + end + + it "returns true if the fork does not exist" do + unlink_project.execute + closed_merge_request.reload + + expect(closed_merge_request.closed_without_fork?).to be_truthy + end + end + + context "when the merge request is open" do + let(:open_merge_request) do + create(:merge_request, + source_project: fork_project, + target_project: project) + end + + it "returns false" do + expect(open_merge_request.closed_without_fork?).to be_falsey + end + end + end end diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb new file mode 100644 index 00000000000..b76513d2a3c --- /dev/null +++ b/spec/models/network/graph_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Network::Graph, models: true do + let(:project) { create(:project) } + let!(:note_on_commit) { create(:note_on_commit, project: project) } + + it '#initialize' do + graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil) + + expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } ) + end +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index ef2747046b9..9e8ae07e0b2 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -225,7 +225,7 @@ describe Note, models: true do let(:note) do create :note, noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d1f3a815290..dd309ea1b68 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -247,7 +247,7 @@ describe Project, models: true do end end - describe "#new_issue_address" do + xdescribe "#new_issue_address" do let(:project) { create(:empty_project, path: "somewhere") } let(:user) { create(:user) } @@ -506,6 +506,18 @@ describe Project, models: true do end end + describe '#has_wiki?' do + let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } + let(:wiki_enabled_project) { build(:project, wiki_enabled: true) } + let(:external_wiki_project) { build(:project, has_external_wiki: true) } + + it 'returns true if project is wiki enabled or has external wiki' do + expect(wiki_enabled_project).to have_wiki + expect(external_wiki_project).to have_wiki + expect(no_wiki_project).not_to have_wiki + end + end + describe '#external_wiki' do let(:project) { create(:project) } @@ -685,23 +697,37 @@ describe Project, models: true do end end - describe '#pipeline' do - let(:project) { create :project } - let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } - - subject { project.pipeline(pipeline.sha, 'master') } + describe '#pipeline_for' do + let(:project) { create(:project) } + let!(:pipeline) { create_pipeline } - it { is_expected.to eq(pipeline) } + shared_examples 'giving the correct pipeline' do + it { is_expected.to eq(pipeline) } - context 'return latest' do - let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } + context 'return latest' do + let!(:pipeline2) { create_pipeline } - before do - pipeline - pipeline2 + it { is_expected.to eq(pipeline2) } end + end + + context 'with explicit sha' do + subject { project.pipeline_for('master', pipeline.sha) } + + it_behaves_like 'giving the correct pipeline' + end + + context 'with implicit sha' do + subject { project.pipeline_for('master') } + + it_behaves_like 'giving the correct pipeline' + end - it { is_expected.to eq(pipeline2) } + def create_pipeline + create(:ci_pipeline, + project: project, + ref: 'master', + sha: project.commit('master').sha) end end @@ -1442,4 +1468,35 @@ describe Project, models: true do expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) end end + + describe 'change_head' do + let(:project) { create(:project) } + + it 'calls the before_change_head method' do + expect(project.repository).to receive(:before_change_head) + project.change_head(project.default_branch) + end + + it 'creates the new reference with rugged' do + expect(project.repository.rugged.references).to receive(:create).with('HEAD', + "refs/heads/#{project.default_branch}", + force: true) + project.change_head(project.default_branch) + end + + it 'copies the gitattributes' do + expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch) + project.change_head(project.default_branch) + end + + it 'expires the avatar cache' do + expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) + project.change_head(project.default_branch) + end + + it 'reloads the default branch' do + expect(project).to receive(:reload_default_branch) + project.change_head(project.default_branch) + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f7dbfd712cc..1fea50ad42c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -719,6 +719,14 @@ describe Repository, models: true do expect(merge_commit).to be_present expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present end + + it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do + merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) + merge_commit_id = repository.merge(user, merge_request, commit_options) + repository.commit(merge_commit_id) + + expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) + end end describe '#revert' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 51e4780e2b1..8eb0c5033c9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -920,6 +920,16 @@ describe User, models: true do expect(subject.recent_push).to eq(nil) end + + it "includes push events on any of the provided projects" do + expect(subject.recent_push(project1)).to eq(nil) + expect(subject.recent_push(project2)).to eq(push_event) + + push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) + push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1) + + expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest + end end describe '#authorized_groups' do diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index c65510fadec..bbdf8f03c2b 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::Helpers, api: true do include API::Helpers include ApiHelpers + include SentryHelper let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -234,4 +235,30 @@ describe API::Helpers, api: true do 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) + allow_any_instance_of(self.class).to receive(:rack_response) + end + + it 'does not report a MethodNotAllowed exception to Sentry' do + exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' }) + allow(exception).to receive(:backtrace).and_return(caller) + + expect(Raven).not_to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + + it 'does report RuntimeError to Sentry' do + exception = RuntimeError.new('test error') + allow(exception).to receive(:backtrace).and_return(caller) + + expect_any_instance_of(self.class).to receive(:sentry_context) + expect(Raven).to receive(:capture_exception).with(exception) + + handle_api_exception(exception) + end + end end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 73c268c0d1e..981a6791881 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -4,7 +4,7 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project) } - let(:issue) { create(:issue, project: project, author: user) } + let(:issue) { create(:issue, project: project) } let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } @@ -115,6 +115,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + context "on an issue" do it "creates a new award emoji" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' @@ -136,6 +138,12 @@ describe API::API, api: true do expect(response).to have_http_status(401) end + it "returns a 404 error if the user authored issue" do + post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' @@ -155,6 +163,8 @@ describe API::API, api: true do end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + let(:note2) { create(:note, project: project, noteable: issue, author: user) } + it 'creates a new award emoji' do expect do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' @@ -164,6 +174,12 @@ describe API::API, api: true do expect(json_response['user']['username']).to eq(user.username) end + it "it returns 404 error when user authored note" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 9a17a705b1e..ee0b61e2ca4 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -15,7 +15,9 @@ describe API::API, api: true do describe 'GET /projects/:id/builds ' do let(:query) { '' } - before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + before do + get api("/projects/#{project.id}/builds?#{query}", api_user) + end context 'authorized user' do it 'returns project builds' do @@ -122,7 +124,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id' do - before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}", api_user) + end context 'authorized user' do it 'returns specific build data' do @@ -141,7 +145,9 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/artifacts' do - before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) + end context 'build with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -292,7 +298,9 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/cancel' do - before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) + end context 'authorized user' do context 'user with :update_build persmission' do @@ -323,7 +331,9 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + before do + post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) + end context 'authorized user' do context 'user with :update_build permission' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 7ca75d77673..5b3dc60aba2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -95,7 +95,7 @@ describe API::API, api: true do end it "returns status for CI" do - pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline('master', project.repository.commit.sha) pipeline.update(status: 'success') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -105,7 +105,7 @@ describe API::API, api: true do end it "returns status for CI when pipeline is created" do - project.ensure_pipeline(project.repository.commit.sha, 'master') + project.ensure_pipeline('master', project.repository.commit.sha) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index be52f88831f..5d06abcfeb3 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -38,6 +38,68 @@ describe API::API, api: true do end end + describe 'GET /internal/two_factor_recovery_codes' do + it 'returns an error message when the key does not exist' do + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: 12345 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + + it 'returns an error message when the key is a deploy key' do + deploy_key = create(:deploy_key) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: deploy_key.id + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes') + end + + it 'returns an error message when the user does not exist' do + key_without_user = create(:key, user: nil) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key_without_user.id + + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find a user for the given key') + expect(json_response['recovery_codes']).to be_nil + end + + context 'when two-factor is enabled' do + it 'returns new recovery codes when the user exists' do + allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true) + allow_any_instance_of(User) + .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key.id + + expect(json_response['success']).to be_truthy + expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861)) + end + end + + context 'when two-factor is not enabled' do + it 'returns an error message' do + allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false) + + post api('/internal/two_factor_recovery_codes'), + secret_token: secret_token, + key_id: key.id + + expect(json_response['success']).to be_falsey + expect(json_response['recovery_codes']).to be_nil + end + end + end + describe "GET /internal/discover" do it do get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index a40e1a93b71..3362a88d798 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } @@ -61,6 +62,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['title']).to eq(issue.title) + expect(json_response.last).to have_key('web_url') end it "adds pagination headers and keep query params" do @@ -477,6 +479,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq(['label', 'label2']) end + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2) + + perform_enqueued_jobs do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end + + should_email(user2) + end + it "returns a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' expect(response).to have_http_status(400) @@ -632,6 +646,18 @@ describe API::API, api: true do expect(json_response['labels']).to eq([label.title]) end + it "sends notifications for subscribers of newly added labels when issue is updated" do + label = create(:label, title: 'foo', color: '#FFAABB', project: project) + label.toggle_subscription(user2) + + perform_enqueued_jobs do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', labels: label.title + end + + should_email(user2) + end + it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index a56ee30f7b1..1e365bf353a 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -122,12 +122,13 @@ describe API::Members, api: true do it 'creates a new member' do expect do post api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: Member::DEVELOPER + user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' expect(response).to have_http_status(201) end.to change { source.members.count }.by(1) expect(json_response['id']).to eq(stranger.id) expect(json_response['access_level']).to eq(Member::DEVELOPER) + expect(json_response['expires_at']).to eq('2016-08-05') end end @@ -183,11 +184,12 @@ describe API::Members, api: true do context 'when authenticated as a master/owner' do it 'updates the member' do put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: Member::MASTER + access_level: Member::MASTER, expires_at: '2016-08-05' expect(response).to have_http_status(200) expect(json_response['id']).to eq(developer.id) expect(json_response['access_level']).to eq(Member::MASTER) + expect(json_response['expires_at']).to eq('2016-08-05') end end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb new file mode 100644 index 00000000000..8f1e5ac9891 --- /dev/null +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -0,0 +1,49 @@ +require "spec_helper" + +describe API::API, 'MergeRequestDiffs', api: true do + include ApiHelpers + + let!(:user) { create(:user) } + let!(:merge_request) { create(:merge_request, importing: true) } + let!(:project) { merge_request.target_project } + + before do + merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') + project.team << [user, :master] + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) } + it { expect(json_response.first['id']).to eq(merge_request_diff.id) } + it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/999/versions", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + context 'valid merge request' do + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) } + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + + it { expect(response.status).to eq 200 } + it { expect(json_response['id']).to eq(merge_request_diff.id) } + it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) } + it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) } + end + + it 'returns a 404 when merge_request_id not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 617600d6173..baff872e28e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -33,6 +33,7 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) + expect(json_response.last).to have_key('web_url') end it "returns an array of all merge_requests" do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 737fa14cbb0..223444ea39f 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -25,7 +25,7 @@ describe API::API, api: true do let!(:cross_reference_note) do create :note, noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}", system: true end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 914e88c9487..765dc8a8f66 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -34,6 +34,7 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response.first['note_events']).to eq(true) expect(json_response.first['build_events']).to eq(true) expect(json_response.first['pipeline_events']).to eq(true) + expect(json_response.first['wiki_page_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) end end @@ -57,6 +58,9 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end @@ -93,6 +97,7 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['note_events']).to eq(false) expect(json_response['build_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false) + expect(json_response['wiki_page_events']).to eq(false) expect(json_response['enable_ssl_verification']).to eq(true) end @@ -118,6 +123,9 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['pipeline_events']).to eq(hook.pipeline_events) + expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 42757ff21b0..01148f0a05e 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -30,6 +30,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response.size).to eq(3) expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + expect(json_response.last).to have_key('web_url') end it 'hides private snippets from regular user' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4742b3d0e37..63f2467be63 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -224,7 +224,8 @@ describe API::API, api: true do description: FFaker::Lorem.sentence, issues_enabled: false, merge_requests_enabled: false, - wiki_enabled: false + wiki_enabled: false, + only_allow_merge_if_build_succeeds: false }) post api('/projects', user), project @@ -276,6 +277,18 @@ describe API::API, api: true do expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end + it 'sets a project as allowing merge even if build fails' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + end + + it 'sets a project as allowing merge only if build succeeds' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + context 'when a visibility level is restricted' do before do @project = attributes_for(:project, { public: true }) @@ -384,6 +397,18 @@ describe API::API, api: true do expect(json_response['public']).to be_falsey expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end + + it 'sets a project as allowing merge even if build fails' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + post api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + end + + it 'sets a project as allowing merge only if build succeeds' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end end describe "POST /projects/:id/uploads" do @@ -444,6 +469,7 @@ describe API::API, api: true do expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) end it 'returns a project by path name' do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 4c9b4a8ba42..fcd6521317a 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -44,6 +44,113 @@ describe 'Git LFS API and storage' do end end + context 'project specific LFS settings' do + let(:project) { create(:empty_project) } + let(:body) do + { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ], + 'operation' => 'upload' + } + end + let(:authorization) { authorize_user } + + context 'with LFS disabled globally' do + before do + project.team << [user, :master] + allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 501 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(501) + end + + it 'responds with a 501 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(501) + end + end + end + + context 'with LFS enabled globally' do + before do + project.team << [user, :master] + enable_lfs + end + + describe 'LFS disabled in project' do + before do + project.update_attribute(:lfs_enabled, false) + end + + it 'responds with a 403 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + + it 'responds with a 403 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(403) + expect(json_response).to include('message' => 'Access forbidden. Check your access level.') + end + end + + describe 'LFS enabled in project' do + before do + project.update_attribute(:lfs_enabled, true) + end + + it 'responds with a 200 message on upload' do + post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers + + expect(response).to have_http_status(200) + expect(json_response['objects'].first['size']).to eq(1575078) + end + + it 'responds with a 200 message on download' do + get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers + + expect(response).to have_http_status(200) + end + end + end + end + describe 'deprecated API' do let(:project) { create(:empty_project) } diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb new file mode 100644 index 00000000000..e02f0eacc93 --- /dev/null +++ b/spec/requests/projects/artifacts_controller_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Projects::ArtifactsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: 'success') + end + + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do + before do + project.team << [user, :developer] + + login_as(user) + end + + def path_from_ref( + ref = pipeline.ref, job = build.name, path = 'browse') + latest_succeeded_namespace_project_artifacts_path( + project.namespace, + project, + [ref, path].join('/'), + job: job) + end + + context 'cannot find the build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get path_from_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_from_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + + context 'has no path' do + before do + get path_from_ref(pipeline.sha, build.name, '') + end + + it_behaves_like 'not found' + end + end + + context 'found the build and redirect' do + shared_examples 'redirect to the build' do + it 'redirects' do + path = browse_namespace_project_build_artifacts_path( + project.namespace, + project, + build) + + expect(response).to redirect_to(path) + end + end + + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_from_ref('master') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome') + end + + it_behaves_like 'redirect to the build' + end + + context 'with branch name and path containing slashes' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + + get path_from_ref('improve/awesome', build.name, 'file/README.md') + end + + it 'redirects' do + path = file_namespace_project_build_artifacts_path( + project.namespace, + project, + build, + 'README.md') + + expect(response).to redirect_to(path) + end + end + end + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1d4df9197f6..4bc3cddd9c2 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -107,21 +107,28 @@ describe HelpController, "routing" do end it 'to #show' do - path = '/help/markdown/markdown.md' + path = '/help/user/markdown.md' expect(get(path)).to route_to('help#show', - path: 'markdown/markdown', + path: 'user/markdown', format: 'md') path = '/help/workflow/protected_branches/protected_branches1.png' expect(get(path)).to route_to('help#show', path: 'workflow/protected_branches/protected_branches1', format: 'png') - + path = '/help/ui' expect(get(path)).to route_to('help#ui') end end +# koding GET /koding(.:format) koding#index +describe KodingController, "routing" do + it "to #index" do + expect(get("/koding")).to route_to('koding#index') + end +end + # profile_account GET /profile/account(.:format) profile#account # profile_history GET /profile/history(.:format) profile#history # profile_password PUT /profile/password(.:format) profile#password_update diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index f7f45983d26..cf4c5f13635 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) } let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } let!(:closed_issue3) { create(:issue, :closed, project: project) } - let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } + let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) } before do project.team << [user, :developer] @@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1] end - it 'returns opened issues that have label list applied when listing issues from a label list' do + it 'returns opened/closed issues that have label list applied when listing issues from a label list' do params = { id: list1.id } issues = described_class.new(project, user, params).execute - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2] end end end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 5e7e145065e..90764b86b16 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -5,7 +5,7 @@ describe Boards::Lists::CreateService, services: true do let(:project) { create(:project_with_board) } let(:board) { project.board } let(:user) { create(:user) } - let(:label) { create(:label, name: 'in-progress') } + let(:label) { create(:label, project: project, name: 'in-progress') } subject(:service) { described_class.new(project, user, label_id: label.id) } @@ -50,5 +50,14 @@ describe Boards::Lists::CreateService, services: true do expect(list2.reload.position).to eq 1 end end + + context 'when provided label does not belongs to the project' do + it 'raises an error' do + label = create(:label, name: 'in-development') + service = described_class.new(project, user, label_id: label.id) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end end end diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index c931c3e4829..b3e0a7b9b58 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,7 +5,7 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:pipeline) { project.ensure_pipeline('master', commit_sha) } let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ad8c2485888..8326e5cd313 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' describe Ci::ProcessPipelineService, services: true do let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:user) { create(:user) } - let(:all_builds) { pipeline.builds } - let(:builds) { all_builds.where.not(status: [:created, :skipped]) } let(:config) { nil } before do @@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do end describe '#execute' do + def all_builds + pipeline.builds + end + + def builds + all_builds.where.not(status: [:created, :skipped]) + end + def create_builds described_class.new(pipeline.project, user).execute(pipeline) end @@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do it 'does not process pipeline if existing stage is running' do expect(create_builds).to be_truthy expect(builds.pending.count).to eq(2) - + expect(create_builds).to be_falsey expect(builds.pending.count).to eq(2) end @@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do end end + context 'when failed build in the middle stage is retried' do + context 'when failed build is the only unsuccessful build in the stage' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2) + end + + it 'does trigger builds in the next stage' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') + + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + pipeline.builds.find_by(name: 'test:1').success + pipeline.builds.find_by(name: 'test:2').drop + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + + expect(builds.pluck(:name)).to contain_exactly( + 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') + end + end + end + context 'creates a builds from .gitlab-ci.yml' do let(:config) do YAML.dump({ diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index c4b87468275..807f89e80b7 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::MergeRequestDiffCacheService do describe '#execute' do it 'retrieves the diff files to cache the highlighted result' do merge_request = create(:merge_request) - cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options] + cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options] expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb new file mode 100644 index 00000000000..d71932458fa --- /dev/null +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe MergeRequests::ResolveService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:fork_project) do + create(:forked_project_with_submodules) do |fork_project| + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + end + end + + let(:merge_request) do + create(:merge_request, + source_branch: 'conflict-resolvable', source_project: project, + target_branch: 'conflict-start') + end + + let(:merge_request_from_fork) do + create(:merge_request, + source_branch: 'conflict-resolvable-fork', source_project: fork_project, + target_branch: 'conflict-start', target_project: project) + end + + describe '#execute' do + context 'with valid params' do + let(:params) do + { + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + }, + commit_message: 'This is a commit message!' + } + end + + context 'when the source and target project are the same' do + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + end + end + + context 'when the source project is a fork and does not contain the HEAD of the target branch' do + let!(:target_head) do + project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + end + + before do + MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + end + + it 'creates a commit with the message' do + expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). + to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', + target_head]) + end + end + end + + context 'when a resolution is missing' do + let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 00427d6db2a..3d854a959f3 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -330,13 +330,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project2.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}" end end end @@ -346,13 +346,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}" end end end @@ -362,7 +362,7 @@ describe SystemNoteService, services: true do describe '.cross_reference?' do it 'is truthy when text begins with expected text' do - expect(described_class.cross_reference?('mentioned in something')).to be_truthy + expect(described_class.cross_reference?('Mentioned in something')).to be_truthy end it 'is falsey when text does not begin with expected text' do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 296fd1bd5a4..cafcad3e3c0 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -496,6 +496,7 @@ describe TodoService, services: true do describe '#mark_todos_as_done' do let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) } + let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) } it 'marks a relation of todos as done' do create(:todo, :mentioned, user: john_doe, target: issue, project: project) @@ -518,6 +519,26 @@ describe TodoService, services: true do expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1) end + context 'when some of the todos are done already' do + before do + create(:todo, :mentioned, user: john_doe, target: issue, project: project) + create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + end + + it 'returns the number of those still pending' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1) + end + + it 'returns 0 if all are done' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + TodoService.new.mark_pending_todos_as_done(another_issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0) + end + end + it 'caches the number of todos of a user', :caching do create(:todo, :mentioned, user: john_doe, target: issue, project: project) todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index 6f8f7109e14..b507d38f472 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -1,4 +1,5 @@ require 'simplecov' +require 'active_support/core_ext/numeric/time' module SimpleCovEnv extend self @@ -48,7 +49,7 @@ module SimpleCovEnv add_group 'Uploaders', 'app/uploaders' add_group 'Validators', 'app/validators' - merge_timeout 7200 + merge_timeout 365.days end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2e2aa7c4fc0..02b2b3ca101 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,13 +26,14 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::TestHelpers, type: :controller - config.include LoginHelpers, type: :feature - config.include LoginHelpers, type: :request + config.include Devise::TestHelpers, type: :controller + config.include Warden::Test::Helpers, type: :request + config.include LoginHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include TestEnv config.include ActiveJob::TestHelper + config.include ActiveSupport::Testing::TimeHelpers config.include StubGitlabCalls config.include StubGitlabData diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 927c72c7409..201614e45a4 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -3,30 +3,57 @@ # Requires a context containing: # subject { Issue or MergeRequest } shared_examples 'a Taskable' do - before do - subject.description = <<-EOT.strip_heredoc - * [ ] Task 1 - * [x] Task 2 - * [x] Task 3 - * [ ] Task 4 - * [ ] Task 5 - EOT + describe 'with multiple tasks' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + * [x] Task 2 + * [x] Task 3 + * [ ] Task 4 + * [ ] Task 5 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('2 of') + expect(subject.task_status).to match('5 tasks completed') + end + + describe '#tasks?' do + it 'returns true when object has tasks' do + expect(subject.tasks?).to eq true + end + + it 'returns false when object has no tasks' do + subject.description = 'Now I have no tasks' + expect(subject.tasks?).to eq false + end + end end - it 'returns the correct task status' do - expect(subject.task_status).to match('5 tasks') - expect(subject.task_status).to match('2 completed') - expect(subject.task_status).to match('3 remaining') + describe 'with an incomplete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [ ] Task 1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('0 of') + expect(subject.task_status).to match('1 task completed') + end end - describe '#tasks?' do - it 'returns true when object has tasks' do - expect(subject.tasks?).to eq true + describe 'with a complete task' do + before do + subject.description = <<-EOT.strip_heredoc + * [x] Task 1 + EOT end - it 'returns false when object has no tasks' do - subject.description = 'Now I have no tasks' - expect(subject.tasks?).to eq false + it 'returns the correct task status' do + expect(subject.task_status).to match('1 of') + expect(subject.task_status).to match('1 task completed') end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index edbbfc3c9e5..0097dbf8fad 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,7 +6,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'empty-branch' => '7efb185', - 'ends-with.json' => '98b0d8b3', + 'ends-with.json' => '98b0d8b', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', @@ -24,11 +24,12 @@ module TestEnv 'expand-collapse-lines' => '238e82d', 'video' => '8879059', 'crlf-diff' => '5938907', - 'conflict-start' => '14fa46b', + 'conflict-start' => '75284c7', 'conflict-resolvable' => '1450cd6', 'conflict-binary-file' => '259a6fb', 'conflict-contains-conflict-markers' => '5e0964c', 'conflict-missing-side' => 'eb227b3', + 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', } @@ -36,9 +37,10 @@ module TestEnv # need to keep all the branches in sync. # We currently only need a subset of the branches FORKED_BRANCH_SHA = { - 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0', - 'remove-submodule' => '2a33e0c0' + 'add-submodule-version-bump' => '3f547c0', + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c', + 'conflict-resolvable-fork' => '404fa3f' } # Test environment @@ -116,22 +118,7 @@ module TestEnv system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end - Dir.chdir(repo_path) do - branch_sha.each do |branch, sha| - # Try to reset without fetching to avoid using the network. - reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) - unless system(*reset) - if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - unless system(*reset) - raise 'The fetched test seed '\ - 'does not contain the required revision.' - end - else - raise 'Could not fetch test seed repository.' - end - end - end - end + set_repo_refs(repo_path, branch_sha) # We must copy bare repositories because we will push to them. system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare})) @@ -143,6 +130,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, BRANCH_SHA) end def repos_path @@ -159,6 +147,7 @@ module TestEnv FileUtils.mkdir_p(target_repo_path) FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path + set_repo_refs(target_repo_path, FORKED_BRANCH_SHA) end # When no cached assets exist, manually hit the root path to create them @@ -208,4 +197,23 @@ module TestEnv def git_env { 'GIT_TEMPLATE_DIR' => '' } end + + def set_repo_refs(repo_path, branch_sha) + Dir.chdir(repo_path) do + branch_sha.each do |branch, sha| + # Try to reset without fetching to avoid using the network. + reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) + unless system(*reset) + if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) + unless system(*reset) + raise 'The fetched test seed '\ + 'does not contain the required revision.' + end + else + raise 'Could not fetch test seed repository.' + end + end + end + end + end end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb new file mode 100644 index 00000000000..31bbb150698 --- /dev/null +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'projects/merge_requests/edit.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:current_user) + .and_return(User.find(closed_merge_request.author_id)) + end + + context 'when a merge request without fork' do + it "shows editable fields" do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) + end + end + + context 'when a merge request with an existing source project is closed' do + it "shows editable fields" do + render + + expect(rendered).to have_field('merge_request[title]') + expect(rendered).to have_field('merge_request[description]') + expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('#merge_request_target_branch', visible: false) + end + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb new file mode 100644 index 00000000000..fe0780e72df --- /dev/null +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show.html.haml' do + include Devise::TestHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + + let(:closed_merge_request) do + create(:closed_merge_request, + source_project: fork_project, + target_project: project, + author: user) + end + + before do + assign(:project, project) + assign(:merge_request, closed_merge_request) + assign(:commits_count, 0) + + allow(view).to receive(:can?).and_return(true) + end + + context 'when the merge request is closed' do + it 'shows the "Reopen" button' do + render + + expect(rendered).to have_css('a', visible: true, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + + it 'does not show the "Reopen" button when the source project does not exist' do + unlink_project.execute + closed_merge_request.reload + + render + + expect(rendered).to have_css('a', visible: false, text: 'Reopen') + expect(rendered).to have_css('a', visible: false, text: 'Close') + end + end +end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index eecc32875a5..7ca2c29da1c 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -2,19 +2,19 @@ require 'spec_helper' describe EmailsOnPushWorker do include RepoHelpers + include EmailSpec::Matchers let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } let(:recipients) { user.email } let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) } + let(:email) { ActionMailer::Base.deliveries.last } subject { EmailsOnPushWorker.new } describe "#perform" do context "when push is a new branch" do - let(:email) { ActionMailer::Base.deliveries.last } - before do data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA) @@ -31,8 +31,6 @@ describe EmailsOnPushWorker do end context "when push is a deleted branch" do - let(:email) { ActionMailer::Base.deliveries.last } - before do data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA) @@ -48,15 +46,40 @@ describe EmailsOnPushWorker do end end - context "when there are no errors in sending" do - let(:email) { ActionMailer::Base.deliveries.last } + context "when push is a force push to delete commits" do + before do + data_force_push = data.stringify_keys.merge( + "after" => data[:before], + "before" => data[:after] + ) + + subject.perform(project.id, recipients, data_force_push) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include('Change some files') + end + it "mentions force pushing in the body" do + expect(email).to have_body_text("force push") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + + context "when there are no errors in sending" do before { perform } it "sends a mail with the correct subject" do expect(email.subject).to include('Change some files') end + it "does not mention force pushing in the body" do + expect(email).not_to have_body_text("force push") + end + it "sends the mail to the correct recipient" do expect(email.to).to eq([user.email]) end @@ -66,6 +89,7 @@ describe EmailsOnPushWorker do before do ActionMailer::Base.deliveries.clear allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + allow(subject).to receive_message_chain(:logger, :info) perform end diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb new file mode 100644 index 00000000000..689bc3d27b4 --- /dev/null +++ b/spec/workers/remove_expired_group_links_worker_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe RemoveExpiredGroupLinksWorker do + describe '#perform' do + let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) } + let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) } + let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) } + + it 'removes expired group links' do + expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1) + expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil + end + + it 'leaves group links that expire in the future' do + subject.perform + expect(project_group_link_expiring_in_future.reload).to be_present + end + + it 'leaves group links that do not expire at all' do + subject.perform + expect(non_expiring_project_group_link.reload).to be_present + end + end +end diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb new file mode 100644 index 00000000000..402aa1e714e --- /dev/null +++ b/spec/workers/remove_expired_members_worker_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe RemoveExpiredMembersWorker do + let(:worker) { RemoveExpiredMembersWorker.new } + + describe '#perform' do + context 'project members' do + let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } + let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } + let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } + + it 'removes expired members' do + expect { worker.perform }.to change { Member.count }.by(-1) + expect(Member.find_by(id: expired_project_member.id)).to be_nil + end + + it 'leaves members that expire in the future' do + worker.perform + expect(project_member_expiring_in_future.reload).to be_present + end + + it 'leaves members that do not expire at all' do + worker.perform + expect(non_expiring_project_member.reload).to be_present + end + end + + context 'group members' do + let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) } + let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) } + let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) } + + it 'removes expired members' do + expect { worker.perform }.to change { Member.count }.by(-1) + expect(Member.find_by(id: expired_group_member.id)).to be_nil + end + + it 'leaves members that expire in the future' do + worker.perform + expect(group_member_expiring_in_future.reload).to be_present + end + + it 'leaves members that do not expire at all' do + worker.perform + expect(non_expiring_group_member.reload).to be_present + end + end + + context 'when the last group owner expires' do + let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) } + + it 'does not delete the owner' do + worker.perform + expect(expired_group_owner.reload).to be_present + end + end + end +end diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js index c264262ba73..c264262ba73 100755..100644 --- a/vendor/assets/javascripts/Chart.js +++ b/vendor/assets/javascripts/Chart.js diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js index cfa49e72c50..cfa49e72c50 100755..100644 --- a/vendor/assets/javascripts/autosize.js +++ b/vendor/assets/javascripts/autosize.js diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js index 7ba17766b70..7ba17766b70 100755..100644 --- a/vendor/assets/javascripts/jquery.scrollTo.js +++ b/vendor/assets/javascripts/jquery.scrollTo.js |