diff options
746 files changed, 14869 insertions, 7036 deletions
diff --git a/.gitignore b/.gitignore index 1bf9a47aef6..9166512606d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ /vendor/bundle/* /builds/* /shared/* +/.gitlab_workhorse_secret diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be5614520a5..f51d506f64a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ update-knapsack: - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} - - knapsack rspec + - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d paths: @@ -248,6 +248,21 @@ bundler:audit: script: - "bundle exec bundle-audit check --update --ignore OSVDB-115941" +migration paths: + stage: test + <<: *use-db + only: + - master@gitlab-org/gitlab-ce + script: + - git checkout HEAD . + - git fetch --tags + - git checkout v8.5.9 + - 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml' + - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 + - rake db:drop db:create db:schema:load db:seed_fu + - git checkout $CI_BUILD_REF + - rake db:migrate + coverage: stage: post-test services: [] @@ -263,7 +278,6 @@ coverage: - coverage/index.html - coverage/assets/ - # Notify slack in the end notify:slack: diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000000..ac38f0c9521 --- /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 behavior + +(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/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md new file mode 100644 index 00000000000..d2a1eb56423 --- /dev/null +++ b/.gitlab/merge_request_templates/Documentation.md @@ -0,0 +1,14 @@ +See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html. + +## What does this MR do? + +(briefly describe what this MR is about) + +## Moving docs to a new location? + +See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location + +- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. +- [ ] Make sure internal links pointing to the document in question are not broken. +- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. +- [ ] If working on CE, submit an MR to EE with the changes as well. diff --git a/.rubocop.yml b/.rubocop.yml index 282f4539f03..5bd31ccf329 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,8 +5,8 @@ require: inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: 2.1 - # Cop names are not displayed in offense messages by default. Change behavior + TargetRubyVersion: 2.3 + # Cop names are not d§splayed in offense messages by default. Change behavior # by overriding DisplayCopNames, or by giving the -D/--display-cop-names # option. DisplayCopNames: true @@ -192,6 +192,9 @@ Style/FlipFlop: Style/For: Enabled: true +# Checks if there is a magic comment to enforce string literals +Style/FrozenStringLiteralComment: + Enabled: false # Do not introduce global variables. Style/GlobalVars: Enabled: true diff --git a/CHANGELOG b/CHANGELOG index 518e80a360a..11e8cba58b6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,22 +1,177 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.12.0 (unreleased) + - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 + - Only check :can_resolve permission if the note is resolvable + - Add ability to fork to a specific namespace using API. (ritave) + - Cleanup misalignments in Issue list view !6206 + - Prune events older than 12 months. (ritave) + - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell) + - Filter tags by name !6121 + - Give project selection dropdowns responsive width, make non-wrapping. + - Make push events have equal vertical spacing. - Add two-factor recovery endpoint to internal API !5510 + - Pass the "Remember me" value to the U2F authentication form + - 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) + - Instructions for enabling Git packfile bitmaps !6104 + - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint + - Fix pagination on user snippets page + - Fix sorting of issues in API + - Escape search term before passing it to Regexp.new !6241 (winniehell) + - Fix pinned sidebar behavior in smaller viewports !6169 - 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) + - Enable pipeline events by default !6278 + - Move parsing of sidekiq ps into helper !6245 (pascalbetz) + - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) + - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) + - Fix blame table layout width + - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps) + - Request only the LDAP attributes we need !6187 + - Center build stage columns in pipeline overview (ClemMakesApps) + - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps) + - Remove suggested colors hover underline (ClemMakesApps) + - Shorten task status phrase (ClemMakesApps) + - Fix project visibility level fields on settings + - Add hover color to emoji icon (ClemMakesApps) + - Add textarea autoresize after comment (ClemMakesApps) + - Refresh todos count cache when an Issue/MR is deleted + - Fix branches page dropdown sort alignment (ClemMakesApps) + - Hides merge request button on branches page is user doesn't have permissions + - Add white background for no readme container (ClemMakesApps) + - API: Expose issue confidentiality flag. (Robert Schilling) + - Fix markdown anchor icon interaction (ClemMakesApps) + - Test migration paths from 8.5 until current release !4874 + - Replace animateEmoji timeout with eventListener (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) + - Remove prefixes from transition CSS property (ClemMakesApps) - Add Sentry logging to API calls + - Add BroadcastMessage API + - Use 'git update-ref' for safer web commits !6130 + - Sort pipelines requested through the API + - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) + - Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084 + - Show queued time when showing a pipeline !6084 + - 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) + - Use JavaScript tooltips for mentions !5301 (winniehell) + - Add hover state to todos !5361 (winniehell) + - Fix icon alignment of star and fork buttons !5451 (winniehell) + - Fix alignment of icon buttons !5887 (winniehell) + - Fix markdown help references (ClemMakesApps) + - Add last commit time to repo view (ClemMakesApps) + - Fix accessibility and visibility of project list dropdown button !6140 + - Fix missing flash messages on service edit page (airatshigapov) + - Added project specific enable/disable setting for LFS !5997 + - Don't expose a user's token in the `/api/v3/user` API (!6047) + - Remove redundant js-timeago-pending from user activity log (ClemMakesApps) + - Ability to manage project issues, snippets, wiki, merge requests and builds access level + - Remove inconsistent font weight for sidebar's labels (ClemMakesApps) + - Align add button on repository view (ClemMakesApps) + - Fix contributions calendar month label truncation (ClemMakesApps) - Added tests for diff notes + - Add pipeline events to Slack integration !5525 + - Add a button to download latest successful artifacts for branches and tags !5142 + - Remove redundant pipeline tooltips (ClemMakesApps) + - Expire commit info views after one day, instead of two weeks, to allow for user email updates + - Add delimiter to project stars and forks count (ClemMakesApps) + - Fix badge count alignment (ClemMakesApps) + - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell) + - Fix repo title alignment (ClemMakesApps) + - Change update interval of contacted_at + - 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) + - Order award emoji tooltips in order they were added (EspadaV8) + - 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. + - Convert datetime coffeescript spec to ES6 (ClemMakesApps) + - Add merge request versions !5467 + - Change using size to use count and caching it for number of group members. !5935 + - Replace play icon font with svg (ClemMakesApps) - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck) - -v 8.11.2 (unreleased) - - Show "Create Merge Request" widget for push events to fork projects on the source project - -v 8.11.1 (unreleased) - - Does not halt the GitHub import process when an error occurs + - Reduce number of database queries on builds tab + - Wrap text in commit message containers + - Capitalize mentioned issue timeline notes (ClemMakesApps) + - Fix inconsistent checkbox alignment (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 + - Avoid conflict with admin labels when importing GitHub labels + - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 + - Fix repository page ui issues + - Fixed invisible scroll controls on build page on iPhone + - Fix error on raw build trace download for old builds stored in database !4822 + - Refactor the triggers page and documentation !6217 + - Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska) + - Use default clone protocol on "check out, review, and merge locally" help page URL + - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska) + - Allow bulk update merge requests from merge requests index page + - Add notification_settings API calls !5632 (mahcsig) + +v 8.11.6 (unreleased) + +v 8.11.5 + - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087 + - Fix member expiration date picker after update. !6184 + - Fix suggested colors options for new labels in the admin area. !6138 + - Optimize discussion notes resolving and unresolving + - Fix GitLab import button + - Fix confidential issues being exposed as public using gitlab.com export + - Remove gitorious from import_sources. !6180 + - Scope webhooks/services that will run for confidential issues + - Remove gitorious from import_sources + - Fix confidential issues being exposed as public using gitlab.com export + +v 8.11.4 + - Fix resolving conflicts on forks. !6082 + - Fix diff commenting on merge requests created prior to 8.10. !6029 + - Fix pipelines tab layout regression. !5952 + - Fix "Wiki" link not appearing in navigation for projects with external wiki. !6057 + - Do not enforce using hash with hidden key in CI configuration. !6079 + - Fix hover leading space bug in pipeline graph !5980 + - Fix sorting issues by "last updated" doesn't work after import from GitHub + - GitHub importer use default project visibility for non-private projects + - Creating an issue through our API now emails label subscribers !5720 + - Block concurrent updates for Pipeline + - Don't create groups for unallowed users when importing projects + - Fix issue boards leak private label names and descriptions + - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) + - Remove gitorious. !5866 + - Allow compare merge request versions + +v 8.11.3 + - 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 IE11 fork button bug !5982 + - 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 + - Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters + - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) + - Issues filters reset button + +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 - - Change using size to use count and caching it for number of group members + - Fixed enter key in search input not working !5888 + +v 8.11.1 + - Pulled due to packaging error. v 8.11.0 - Use test coverage value from the latest successful pipeline in badge. !5862 @@ -25,7 +180,6 @@ v 8.11.0 - 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) @@ -41,7 +195,6 @@ v 8.11.0 - 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 @@ -77,12 +230,9 @@ v 8.11.0 - Update `timeago` plugin to use multiple string/locale settings - Remove unused images (ClemMakesApps) - Get issue and merge request description templates from repositories - - Add hover state to todos !5361 (winniehell) - - Fix icon alignment of star and fork buttons !5451 (winniehell) - 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 @@ -99,7 +249,6 @@ v 8.11.0 - Fix devise deprecation warnings. - Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764 - Update version_sorter and use new interface for faster tag sorting - - Load branches asynchronously in Cherry Pick and Revert dialogs. - Optimize checking if a user has read access to a list of issues !5370 - Store all DB secrets in secrets.yml, under descriptive names !5274 - Fix syntax highlighting in file editor @@ -120,8 +269,6 @@ v 8.11.0 - 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) @@ -133,14 +280,12 @@ v 8.11.0 - 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) @@ -149,7 +294,6 @@ v 8.11.0 - edit_blob_link will use blob passed onto the options parameter - Make error pages responsive (Takuya Noguchi) - The performance of the project dropdown used for moving issues has been improved - - Move to project dropdown with infinite scroll for better performance - Fix skip_repo parameter being ignored when destroying a namespace - Add all builds into stage/job dropdowns on builds page - Change requests_profiles resource constraint to catch virtually any file @@ -181,7 +325,6 @@ v 8.11.0 - Eliminate unneeded calls to Repository#blob_at when listing commits with no path - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done - - Update merge_requests.md with a simpler way to check out a merge request. !5944 v 8.10.7 - Upgrade Hamlit to 2.6.1. !5873 @@ -1894,1692 +2037,5 @@ v 8.0.0 - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato) - Removed API calls from CE to CI -v 7.14.3 - - No changes - -v 7.14.2 - - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) - - Allow configuration of LDAP attributes GitLab will use for the new user account. - -v 7.14.1 - - Improve abuse reports management from admin area - - Fix "Reload with full diff" URL button in compare branch view (Stan Hu) - - Disabled DNS lookups for SSH in docker image (Rowan Wookey) - - Only include base URL in OmniAuth full_host parameter (Stan Hu) - - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu) - - Ability to enable SSL verification for Webhooks - -v 7.14.0 - - Fix bug where non-project members of the target project could set labels on new merge requests. - - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller) - - Fix redirection after sign in when using auto_sign_in_with_provider - - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu) - - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu) - - Provide more feedback what went wrong if HipChat service failed test (Stan Hu) - - Fix bug where backslashes in inline diffs could be dropped (Stan Hu) - - Disable turbolinks when linking to Bitbucket import status (Stan Hu) - - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu) - - Fix corrupted binary files when using API files endpoint (Stan Hu) - - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu) - - Show incompatible projects in Bitbucket import status (Stan Hu) - - Fix coloring of diffs on MR Discussion-tab (Gert Goet) - - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu) - - Fix errors deleting and creating branches with encoded slashes (Stan Hu) - - Always add current user to autocomplete controller to support filter by "Me" (Stan Hu) - - Fix multi-line syntax highlighting (Stan Hu) - - Fix network graph when branch name has single quotes (Stan Hu) - - Add "Confirm user" button in user admin page (Stan Hu) - - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) - - Add support for Unicode filenames in relative links (Hiroyuki Sato) - - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) - - Fix commit data retrieval when branch name has single quotes (Stan Hu) - - Check that project was actually created rather than just validated in import:repos task (Stan Hu) - - Fix full screen mode for snippet comments (Daniel Gerhardt) - - Fix 404 error in files view after deleting the last file in a repository (Stan Hu) - - Fix the "Reload with full diff" URL button (Stan Hu) - - Fix label read access for unauthenticated users (Daniel Gerhardt) - - Fix access to disabled features for unauthenticated users (Daniel Gerhardt) - - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu) - - Fix file upload dialog for comment editing (Daniel Gerhardt) - - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) - - Return comments in created order in merge request API (Stan Hu) - - Disable internal issue tracker controller if external tracker is used (Stan Hu) - - Expire Rails cache entries after two weeks to prevent endless Redis growth - - Add support for destroying project milestones (Stan Hu) - - Allow custom backup archive permissions - - Add project star and fork count, group avatar URL and user/group web URL attributes to API - - Show who last edited a comment if it wasn't the original author - - Send notification to all participants when MR is merged. - - Add ability to manage user email addresses via the API. - - Show buttons to add license, changelog and contribution guide if they're missing. - - Tweak project page buttons. - - Disabled autocapitalize and autocorrect on login field (Daryl Chan) - - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) - - Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller) - - Remove redis-store TTL monkey patch - - Add support for CI skipped status - - Fetch code from forks to refs/merge-requests/:id/head when merge request created - - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg) - - Add "Check out branch" button to the MR page. - - Improve MR merge widget text and UI consistency. - - Improve text in MR "How To Merge" modal. - - Cache all events - - Order commits by date when comparing branches - - Fix bug causing error when the target branch of a symbolic ref was deleted - - Include branch/tag name in archive file and directory name - - Add dropzone upload progress - - Add a label for merged branches on branches page (Florent Baldino) - - Detect .mkd and .mkdn files as markdown (Ben Boeckel) - - Fix: User search feature in admin area does not respect filters - - Set max-width for README, issue and merge request description for easier read on big screens - - Update Flowdock integration to support new Flowdock API (Boyan Tabakov) - - Remove author from files view (Sven Strickroth) - - Fix infinite loop when SAML was incorrectly configured. - -v 7.13.5 - - Satellites reverted - -v 7.13.4 - - Allow users to send abuse reports - -v 7.13.3 - - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - - Allow users to send abuse reports - - Remove satellites - - Link username to profile on Group Members page (Tom Webster) - -v 7.13.2 - - Fix randomly failed spec - - Create project services on Project creation - - Add admin_merge_request ability to Developer level and up - - Fix Error 500 when browsing projects with no HEAD (Stan Hu) - - Fix labels / assignee / milestone for the merge requests when issues are disabled - - Show the first tab automatically on MergeRequests#new - - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt) - - Fix Gmail Actions - -v 7.13.1 - - Fix: Label modifications are not reflected in existing notes and in the issue list - - Fix: Label not shown in the Issue list, although it's set through web interface - - Fix: Group/project references are linked incorrectly - - Improve documentation - - Fix of migration: Check if session_expire_delay column exists before adding the column - - Fix: ActionView::Template::Error - - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch - - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. - - Render Note field hints consistently for "new" and "edit" forms - -v 7.13.0 - - Remove repository graph log to fix slow cache updates after push event (Stan Hu) - - Only enable HSTS header for HTTPS and port 443 (Stan Hu) - - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu) - - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt) - - Add branch switching support for graphs (Daniel Gerhardt) - - Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt) - - Remove link leading to a 404 error in Deploy Keys page (Stan Hu) - - Add support for unlocking users in admin settings (Stan Hu) - - Add Irker service configuration options (Stan Hu) - - Fix order of issues imported from GitHub (Hiroyuki Sato) - - Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart) - - Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI - - Add `two_factor_enabled` field to admin user API (Stan Hu) - - Fix invalid timestamps in RSS feeds (Rowan Wookey) - - Fix downloading of patches on public merge requests when user logged out (Stan Hu) - - Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu) - - Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu) - - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) - - Support commenting on diffs in side-by-side mode (Stan Hu) - - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) - - Return 40x error codes if branch could not be deleted in UI (Stan Hu) - - Remove project visibility icons from dashboard projects list - - Rename "Design" profile settings page to "Preferences". - - Allow users to customize their default Dashboard page. - - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 - - Admin can edit and remove user identities - - Convert CRLF newlines to LF when committing using the web editor. - - API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged. - - Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled. - - Show a user's Two-factor Authentication status in the administration area. - - Explicit error when commit not found in the CI - - Improve performance for issue and merge request pages - - Users with guest access level can not set assignee, labels or milestones for issue and merge request - - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels - - Better performance for pages with events list, issues list and commits list - - Faster automerge check and merge itself when source and target branches are in same repository - - Correctly show anonymous authorized applications under Profile > Applications. - - Query Optimization in MySQL. - - Allow users to be blocked and unblocked via the API - - Use native Postgres database cleaning during backup restore - - Redesign project page. Show README as default instead of activity. Move project activity to separate page - - Make left menu more hierarchical and less contextual by adding back item at top - - A fork can’t have a visibility level that is greater than the original project. - - Faster code search in repository and wiki. Fixes search page timeout for big repositories - - Allow administrators to disable 2FA for a specific user - - Add error message for SSH key linebreaks - - Store commits count in database (will populate with valid values only after first push) - - Rebuild cache after push to repository in background job - - Fix transferring of project to another group using the API. - -v 7.12.2 - - Correctly show anonymous authorized applications under Profile > Applications. - - Faster automerge check and merge itself when source and target branches are in same repository - - Audit log for user authentication - - Allow custom label to be set for authentication providers. - -v 7.12.1 - - Fix error when deleting a user who has projects (Stan Hu) - - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) - - Add SAML to list of social_provider (Matt Firtion) - - Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets) - - Fix closed merge request scope at milestone page (Dmitriy Zaporozhets) - - Revert merge request states renaming - - Fix hooks for web based events with external issue references (Daniel Gerhardt) - - Improve performance for issue and merge request pages - - Compress database dumps to reduce backup size - -v 7.12.0 - - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) - - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) - - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) - - Update oauth button logos for Twitter and Google to recommended assets - - Update browser gem to version 0.8.0 for IE11 support (Stan Hu) - - Fix timeout when rendering file with thousands of lines. - - Add "Remember me" checkbox to LDAP signin form. - - Add session expiration delay configuration through UI application settings - - Don't notify users mentioned in code blocks or blockquotes. - - Omit link to generate labels if user does not have access to create them (Stan Hu) - - Show warning when a comment will add 10 or more people to the discussion. - - Disable changing of the source branch in merge request update API (Stan Hu) - - Shorten merge request WIP text. - - Add option to disallow users from registering any application to use GitLab as an OAuth provider - - Support editing target branch of merge request (Stan Hu) - - Refactor permission checks with issues and merge requests project settings (Stan Hu) - - Fix Markdown preview not working in Edit Milestone page (Stan Hu) - - Fix Zen Mode not closing with ESC key (Stan Hu) - - Allow HipChat API version to be blank and default to v2 (Stan Hu) - - Add file attachment support in Milestone description (Stan Hu) - - Fix milestone "Browse Issues" button. - - Set milestone on new issue when creating issue from index with milestone filter active. - - Make namespace API available to all users (Stan Hu) - - Add webhook support for note events (Stan Hu) - - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) - - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) - - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) - - Fix git blame syntax highlighting when different commits break up lines (Stan Hu) - - Add "Resend confirmation e-mail" link in profile settings (Stan Hu) - - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) - - Disabled expansion of top/bottom blobs for new file diffs - - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) - - Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka) - - Use the user list from the target project in a merge request (Stan Hu) - - Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen) - - Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen) - - Fix new/empty milestones showing 100% completion value (Jonah Bishop) - - Add a note when an Issue or Merge Request's title changes - - Consistently refer to MRs as either Merged or Closed. - - Add Merged tab to MR lists. - - Prefix EmailsOnPush email subject with `[Git]`. - - Group project contributions by both name and email. - - Clarify navigation labels for Project Settings and Group Settings. - - Move user avatar and logout button to sidebar - - You can not remove user if he/she is an only owner of group - - User should be able to leave group. If not - show him proper message - - User has ability to leave project - - Add SAML support as an omniauth provider - - Allow to configure a URL to show after sign out - - Add an option to automatically sign-in with an Omniauth provider - - GitLab CI service sends .gitlab-ci.yml in each push call - - When remove project - move repository and schedule it removal - - Improve group removing logic - - Trigger create-hooks on backup restore task - - Add option to automatically link omniauth and LDAP identities - - Allow special character in users bio. I.e.: I <3 GitLab - -v 7.11.4 - - Fix missing bullets when creating lists - - Set rel="nofollow" on external links - -v 7.11.3 - - no changes - - Fix upgrader script (Martins Polakovs) - -v 7.11.2 - - no changes - -v 7.11.1 - - no changes - -v 7.11.0 - - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) - - Get editing comments to work in Chrome 43 again. - - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) - - Don't show duplicate deploy keys - - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) - - Make the first branch pushed to an empty repository the default HEAD (Stan Hu) - - Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu) - - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) - - Add application setting to restrict user signups to e-mail domains (Stan Hu) - - Don't allow a merge request to be merged when its title starts with "WIP". - - Add a page title to every page. - - Allow primary email to be set to an email that you've already added. - - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - - Ignore invalid lines in .gitmodules - - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) - - Redirect to sign in page after signing out. - - Fix "Hello @username." references not working by no longer allowing usernames to end in period. - - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu) - - Improve project page UI - - Fix broken file browsing with relative submodule in personal projects (Stan Hu) - - Add "Reply quoting selected text" shortcut key (`r`) - - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention. - - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention. - - When use change branches link at MR form - save source branch selection instead of target one - - Improve handling of large diffs - - Added GitLab Event header for project hooks - - Add Two-factor authentication (2FA) for GitLab logins - - Show Atom feed buttons everywhere where applicable. - - Add project activity atom feed. - - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits. - - Explain how to get a new password reset token in welcome emails - - Include commit comments in MR from a forked project. - - Group milestones by title in the dashboard and all other issue views. - - Query issues, merge requests and milestones with their IID through API (Julien Bianchi) - - Add default project and snippet visibility settings to the admin web UI. - - Show incompatible projects in Google Code import status (Stan Hu) - - Fix bug where commit data would not appear in some subdirectories (Stan Hu) - - Task lists are now usable in comments, and will show up in Markdown previews. - - Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu) - - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) - - Protect OmniAuth request phase against CSRF. - - Don't send notifications to mentioned users that don't have access to the project in question. - - Add search issues/MR by number - - Change plots to bar graphs in commit statistics screen - - Move snippets UI to fluid layout - - Improve UI for sidebar. Increase separation between navigation and content - - Improve new project command options (Ben Bodenmiller) - - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük) - - Prevent sending empty messages to HipChat (Chulki Lee) - - Improve UI for mobile phones on dashboard and project pages - - Add room notification and message color option for HipChat - - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka) - - Add footnotes support to Markdown (Guillaume Delbergue) - - Add current_sign_in_at to UserFull REST api. - - Make Sidekiq MemoryKiller shutdown signal configurable - - Add "Create Merge Request" buttons to commits and branches pages and push event. - - Show user roles by comments. - - Fix automatic blocking of auto-created users from Active Directory. - - Call merge request webhook for each new commits (Arthur Gautier) - - Use SIGKILL by default in Sidekiq::MemoryKiller - - Fix mentioning of private groups. - - Add style for <kbd> element in markdown - - Spin spinner icon next to "Checking for CI status..." on MR page. - - Fix reference links in dashboard activity and ATOM feeds. - - Ensure that the first added admin performs repository imports - -v 7.10.4 - - Fix migrations broken in 7.10.2 - - Make tags for GitLab installations running on MySQL case sensitive - - Get Gitorious importer to work again. - - Fix adding new group members from admin area - - Fix DB error when trying to tag a repository (Stan Hu) - - Fix Error 500 when searching Wiki pages (Stan Hu) - - Unescape branch names in compare commit (Stan Hu) - - Order commit comments chronologically in API. - -v 7.10.2 - - Fix CI links on MR page - -v 7.10.0 - - Ignore submodules that are defined in .gitmodules but are checked in as directories. - - Allow projects to be imported from Google Code. - - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger) - - Allow users to be invited by email to join a group or project. - - Don't crash when project repository doesn't exist. - - Add config var to block auto-created LDAP users. - - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message. - - Set EmailsOnPush reply-to address to committer email when enabled. - - Fix broken file browsing with a submodule that contains a relative link (Stan Hu) - - Fix persistent XSS vulnerability around profile website URLs. - - Fix project import URL regex to prevent arbitary local repos from being imported. - - Fix directory traversal vulnerability around uploads routes. - - Fix directory traversal vulnerability around help pages. - - Don't leak existence of project via search autocomplete. - - Don't leak existence of group or project via search. - - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu) - - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu) - - Add a rake task to check repository integrity with `git fsck` - - Add ability to configure Reply-To address in gitlab.yml (Stan Hu) - - Move current user to the top of the list in assignee/author filters (Stan Hu) - - Fix broken side-by-side diff view on merge request page (Stan Hu) - - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - - Allow HTML tags in Markdown input - - Fix code unfold not working on Compare commits page (Stan Hu) - - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik) - - Fix "Import projects from" button to show the correct instructions (Stan Hu) - - Fix dots in Wiki slugs causing errors (Stan Hu) - - Make maximum attachment size configurable via Application Settings (Stan Hu) - - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) - - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu) - - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - - Fix a link in the patch update guide - - Add a service to support external wikis (Hannes Rosenögger) - - Omit the "email patches" link and fix plain diff view for merge commits - - List new commits for newly pushed branch in activity view. - - Add sidetiq gem dependency to match EE - - Add changelog, license and contribution guide links to project tab bar. - - Improve diff UI - - Fix alignment of navbar toggle button (Cody Mize) - - Fix checkbox rendering for nested task lists - - Identical look of selectboxes in UI - - Upgrade the gitlab_git gem to version 7.1.3 - - Move "Import existing repository by URL" option to button. - - Improve error message when save profile has error. - - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) - - Add location field to user profile - - Fix print view for markdown files and wiki pages - - Fix errors when deleting old backups - - Improve GitLab performance when working with git repositories - - Add tag message and last commit to tag hook (Kamil Trzciński) - - Restrict permissions on backup files - - Improve oauth accounts UI in profile page - - Add ability to unlink connected accounts - - Replace commits calendar with faster contribution calendar that includes issues and merge requests - - Add inifinite scroll to user page activity - - Don't include system notes in issue/MR comment count. - - Don't mark merge request as updated when merge status relative to target branch changes. - - Link note avatar to user. - - Make Git-over-SSH errors more descriptive. - - Fix EmailsOnPush. - - Refactor issue filtering - - AJAX selectbox for issue assignee and author filters - - Fix issue with missing options in issue filtering dropdown if selected one - - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. - - Prevent note form from being cleared when submitting failed. - - Improve file icons rendering on tree (Sullivan Sénéchal) - - API: Add pagination to project events - - Get issue links in notification mail to work again. - - Don't show commit comment button when user is not signed in. - - Fix admin user projects lists. - - Don't leak private group existence by redirecting from namespace controller to group controller. - - Ability to skip some items from backup (database, respositories or uploads) - - Archive repositories in background worker. - - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. - - Project labels are now available over the API under the "tag_list" field (Cristian Medina) - - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) - - Fix and improve help rendering (Sullivan Sénéchal) - - Fix final line in EmailsOnPush email diff being rendered as error. - - Prevent duplicate Buildkite service creation. - - Fix git over ssh errors 'fatal: protocol error: bad line length character' - - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled - - Bust group page project list cache when namespace name or path changes. - - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded - - Allow user to choose a public email to show on public profile - - Remove truncation from issue titles on milestone page (Jason Blanchard) - - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - - Fix merge request comments on files with multiple commits - - Fix Resource Owner Password Authentication Flow - - Add icons to Add dropdown items. - - Allow admin to create public deploy keys that are accessible to any project. - - Warn when gitlab-shell version doesn't match requirement. - - Skip email confirmation when set by admin or via LDAP. - - Only allow users to reference groups, projects, issues, MRs, commits they have access to. - -v 7.9.4 - - Security: Fix project import URL regex to prevent arbitary local repos from being imported - - Fixed issue where only 25 commits would load in file listings - - Fix LDAP identities after config update - -v 7.9.3 - - Contains no changes - -v 7.9.2 - - Contains no changes - -v 7.9.1 - - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - - Fix "Import projects from" button to show the correct instructions (Stan Hu) - - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) - - Fix for LDAP with commas in DN - - Fix missing events and in admin Slack service template settings form (Stan Hu) - - Don't show commit comment button when user is not signed in. - - Downgrade gemnasium-gitlab-service gem - -v 7.9.0 - - Add HipChat integration documentation (Stan Hu) - - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - - Fix broken email images (Hannes Rosenögger) - - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg) - - Fix mass SQL statements on initial push (Hannes Rosenögger) - - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) - - Add comment notification events to HipChat and Slack services (Stan Hu) - - Add issue and merge request events to HipChat and Slack services (Stan Hu) - - Fix merge request URL passed to Webhooks. (Stan Hu) - - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) - - Move labels/milestones tabs to sidebar - - Upgrade Rails gem to version 4.1.9. - - Improve error messages for file edit failures - - Improve UI for commits, issues and merge request lists - - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. - - Allow admins to override restricted project visibility settings. - - Move restricted visibility settings from gitlab.yml into the web UI. - - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) - - Save web edit in new branch - - Fix ordering of imported but unchanged projects (Marco Wessel) - - Mobile UI improvements: make aside content expandable - - Expose avatar_url in projects API - - Fix checkbox alignment on the application settings page. - - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - - Fix mass-unassignment of issues (Robert Speicher) - - Fix hidden diff comments in merge request discussion view - - Allow user confirmation to be skipped for new users via API - - Add a service to send updates to an Irker gateway (Romain Coltel) - - Add brakeman (security scanner for Ruby on Rails) - - Slack username and channel options - - Add grouped milestones from all projects to dashboard. - - Webhook sends pusher email as well as commiter - - Add Bitbucket omniauth provider. - - Add Bitbucket importer. - - Support referencing issues to a project whose name starts with a digit - - Condense commits already in target branch when updating merge request source branch. - - Send notifications and leave system comments when bulk updating issues. - - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) - - Move groups page from profile to dashboard - - Starred projects page at dashboard - - Blocking user does not remove him/her from project/groups but show blocked label - - Change subject of EmailsOnPush emails to include namespace, project and branch. - - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. - - Remove confusing footer from EmailsOnPush mail body. - - Add list of changed files to EmailsOnPush emails. - - Add option to send EmailsOnPush emails from committer email if domain matches. - - Add option to disable code diffs in EmailOnPush emails. - - Wrap commit message in EmailsOnPush email. - - Send EmailsOnPush emails when deleting commits using force push. - - Fix EmailsOnPush email comparison link to include first commit. - - Fix highliht of selected lines in file - - Reject access to group/project avatar if the user doesn't have access. - - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update) - - Add GitLab active users count to rake gitlab:check - - Starred projects page at dashboard - - Make email display name configurable - - Improve json validation in hook data - - Use Emoji One - - Updated emoji help documentation to properly reference EmojiOne. - - Fix missing GitHub organisation repositories on import page. - - Added blue theme - - Remove annoying notice messages when create/update merge request - - Allow smb:// links in Markdown text. - - Filter merge request by title or description at Merge Requests page - - Block user if he/she was blocked in Active Directory - - Fix import pages not working after first load. - - Use custom LDAP label in LDAP signin form. - - Execute hooks and services when branch or tag is created or deleted through web interface. - - Block and unblock user if he/she was blocked/unblocked in Active Directory - - Raise recommended number of unicorn workers from 2 to 3 - - Use same layout and interactivity for project members as group members. - - Prevent gitlab-shell character encoding issues by receiving its changes as raw data. - - Ability to unsubscribe/subscribe to issue or merge request - - Delete deploy key when last connection to a project is destroyed. - - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) - - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) - - Add canceled status for CI - - Send EmailsOnPush email when branch or tag is created or deleted. - - Faster merge request processing for large repository - - Prevent doubling AJAX request with each commit visit via Turbolink - - Prevent unnecessary doubling of js events on import pages and user calendar - -v 7.8.4 - - Fix issue_tracker_id substitution in custom issue trackers - - Fix path and name duplication in namespaces - -v 7.8.3 - - Bump version of gitlab_git fixing annotated tags without message - -v 7.8.2 - - Fix service migration issue when upgrading from versions prior to 7.3 - - Fix setting of the default use project limit via admin UI - - Fix showing of already imported projects for GitLab and Gitorious importers - - Fix response of push to repository to return "Not found" if user doesn't have access - - Fix check if user is allowed to view the file attachment - - Fix import check for case sensetive namespaces - - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. - - Properly handle autosave local storage exceptions. - - Escape wildcards when searching LDAP by username. - -v 7.8.1 - - Fix run of custom post receive hooks - - Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3 - - Fix the warning for LDAP users about need to set password - - Fix avatars which were not shown for non logged in users - - Fix urls for the issues when relative url was enabled - -v 7.8.0 - - Fix access control and protection against XSS for note attachments and other uploads. - - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - - Make project search case insensitive (Hannes Rosenögger) - - Include issue/mr participants in list of recipients for reassign/close/reopen emails - - Expose description in groups API - - Better UI for project services page - - Cleaner UI for web editor - - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger) - - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen) - - View note image attachments in new tab when clicked instead of downloading them - - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default - - Fix overflow at sidebar when have several items - - Add notes for label changes in issue and merge requests - - Show tags in commit view (Hannes Rosenögger) - - Only count a user's vote once on a merge request or issue (Michael Clarke) - - Increase font size when browse source files and diffs - - Service Templates now let you set default values for all services - - Create new file in empty repository using GitLab UI - - Ability to clone project using oauth2 token - - Upgrade Sidekiq gem to version 3.3.0 - - Stop git zombie creation during force push check - - Show success/error messages for test setting button in services - - Added Rubocop for code style checks - - Fix commits pagination - - Async load a branch information at the commit page - - Disable blacklist validation for project names - - Allow configuring protection of the default branch upon first push (Marco Wessel) - - Add gitlab.com importer - - Add an ability to login with gitlab.com - - Add a commit calendar to the user profile (Hannes Rosenögger) - - Submit comment on command-enter - - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. - - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger) - - Fix long broadcast message cut-off on left sidebar (Visay Keo) - - Add Project Avatars (Steven Thonus and Hannes Rosenögger) - - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. - - Edit group members via API - - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks) - - Add action property to merge request hook (Julien Bianchi) - - Remove duplicates from group milestone participants list. - - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) - - API: Access groups with their path (Julien Bianchi) - - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) - - Allow notification email to be set separately from primary email. - - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - - Don't have Markdown preview fail for long comments/wiki pages. - - When test webhook - show error message instead of 500 error page if connection to hook url was reset - - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - - Added persistent collapse button for left side nav bar (Jason Blanchard) - - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - - Don't allow page to be scaled on mobile. - - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. - - Show assignees in merge request index page (Kelvin Mutuma) - - Link head panel titles to relevant root page. - - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). - - Show users button to share their newly created public or internal projects on twitter - - Add quick help links to the GitLab pricing and feature comparison pages. - - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. - - Make sure Markdown previews always use the same styling as the eventual destination. - - Remove deprecated Group#owner_id from API - - Show projects user contributed to on user page. Show stars near project on user page. - - Improve database performance for GitLab - - Add Asana service (Jeremy Benoist) - - Improve project webhooks with extra data - -v 7.7.2 - - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch - - Fix issue when LDAP user can't login with existing GitLab account - -v 7.7.1 - - Improve mention autocomplete performance - - Show setup instructions for GitHub import if disabled - - Allow use http for OAuth applications - -v 7.7.0 - - Import from GitHub.com feature - - Add Jetbrains Teamcity CI service (Jason Lippert) - - Mention notification level - - Markdown preview in wiki (Yuriy Glukhov) - - Raise group avatar filesize limit to 200kb - - OAuth applications feature - - Show user SSH keys in admin area - - Developer can push to protected branches option - - Set project path instead of project name in create form - - Block Git HTTP access after 10 failed authentication attempts - - Updates to the messages returned by API (sponsored by O'Reilly Media) - - New UI layout with side navigation - - Add alert message in case of outdated browser (IE < 10) - - Added API support for sorting projects - - Update gitlab_git to version 7.0.0.rc14 - - Add API project search filter option for authorized projects - - Fix File blame not respecting branch selection - - Change some of application settings on fly in admin area UI - - Redesign signin/signup pages - - Close standard input in Gitlab::Popen.popen - - Trigger GitLab CI when push tags - - When accept merge request - do merge using sidaekiq job - - Enable web signups by default - - Fixes for diff comments: drag-n-drop images, selecting images - - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update - - Remove password strength indicator - -v 7.6.0 - - Fork repository to groups - - New rugged version - - Add CRON=1 backup setting for quiet backups - - Fix failing wiki restore - - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable) - - Monokai highlighting style now more faithful to original design (Mark Riedesel) - - Create project with repository in synchrony - - Added ability to create empty repo or import existing one if project does not have repository - - Reactivate highlight.js language autodetection - - Mobile UI improvements - - Change maximum avatar file size from 100KB to 200KB - - Strict validation for snippet file names - - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada) - - In the docker directory is a container template based on the Omnibus packages. - - Update Sidekiq to version 2.17.8 - - Add author filter to project issues and merge requests pages - - Atom feed for user activity - - Support multiple omniauth providers for the same user - - Rendering cross reference in issue title and tooltip for merge request - - Show username in comments - - Possibility to create Milestones or Labels when Issues are disabled - - Fix bug with showing gpg signature in tag - -v 7.5.3 - - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) - -v 7.5.2 - - Don't log Sidekiq arguments by default - - Fix restore of wiki repositories from backups - -v 7.5.1 - - Add missing timestamps to 'members' table - -v 7.5.0 - - API: Add support for Hipchat (Kevin Houdebert) - - Add time zone configuration in gitlab.yml (Sullivan Senechal) - - Fix LDAP authentication for Git HTTP access - - Run 'GC.start' after every EmailsOnPushWorker job - - Fix LDAP config lookup for provider 'ldap' - - Drop all sequences during Postgres database restore - - Project title links to project homepage (Ben Bodenmiller) - - Add Atlassian Bamboo CI service (Drew Blessing) - - Mentioned @user will receive email even if he is not participating in issue or commit - - Session API: Use case-insensitive authentication like in UI (Andrey Krivko) - - Tie up loose ends with annotated tags: API & UI (Sean Edge) - - Return valid json for deleting branch via API (sponsored by O'Reilly Media) - - Expose username in project events API (sponsored by O'Reilly Media) - - Adds comments to commits in the API - - Performance improvements - - Fix post-receive issue for projects with deleted forks - - New gitlab-shell version with custom hooks support - - Improve code - - GitLab CI 5.2+ support (does not support older versions) - - Fixed bug when you can not push commits starting with 000000 to protected branches - - Added a password strength indicator - - Change project name and path in one form - - Display renamed files in diff views (Vinnie Okada) - - Fix raw view for public snippets - - Use secret token with GitLab internal API. - - Add missing timestamps to 'members' table - -v 7.4.5 - - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) - -v 7.4.4 - - No changes - -v 7.4.3 - - Fix raw snippets view - - Fix security issue for member api - - Fix buildbox integration - -v 7.4.2 - - Fix internal snippet exposing for unauthenticated users - -v 7.4.1 - - Fix LDAP authentication for Git HTTP access - - Fix LDAP config lookup for provider 'ldap' - - Fix public snippets - - Fix 500 error on projects with nested submodules - -v 7.4.0 - - Refactored membership logic - - Improve error reporting on users API (Julien Bianchi) - - Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally - - Default branch is protected by default - - Increase unicorn timeout to 60 seconds - - Sort search autocomplete projects by stars count so most popular go first - - Add README to tab on project show page - - Do not delete tmp/repositories itself during clean-up, only its contents - - Support for backup uploads to remote storage - - Prevent notes polling when there are not notes - - Internal ForkService: Prepare support for fork to a given namespace - - API: Add support for forking a project via the API (Bernhard Kaindl) - - API: filter project issues by milestone (Julien Bianchi) - - Fail harder in the backup script - - Changes to Slack service structure, only webhook url needed - - Zen mode for wiki and milestones (Robert Schilling) - - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling) - - Font Awesome 4.2 integration (Sullivan Senechal) - - Add Pushover service integration (Sullivan Senechal) - - Add select field type for services options (Sullivan Senechal) - - Add cross-project references to the Markdown parser (Vinnie Okada) - - Add task lists to issue and merge request descriptions (Vinnie Okada) - - Snippets can be public, internal or private - - Improve danger zone: ask project path to confirm data-loss action - - Raise exception on forgery - - Show build coverage in Merge Requests (requires GitLab CI v5.1) - - New milestone and label links on issue edit form - - Improved repository graphs - - Improve event note display in dashboard and project activity views (Vinnie Okada) - - Add users sorting to admin area - - UI improvements - - Fix ambiguous sha problem with mentioned commit - - Fixed bug with apostrophe when at mentioning users - - Add active directory ldap option - - Developers can push to wiki repo. Protected branches does not affect wiki repo any more - - Faster rev list - - Fix branch removal - -v 7.3.2 - - Fix creating new file via web editor - - Use gitlab-shell v2.0.1 - -v 7.3.1 - - Fix ref parsing in Gitlab::GitAccess - - Fix error 500 when viewing diff on a file with changed permissions - - Fix adding comments to MR when source branch is master - - Fix error 500 when searching description contains relative link - -v 7.3.0 - - Always set the 'origin' remote in satellite actions - - Write authorized_keys in tmp/ during tests - - Use sockets to connect to Redis - - Add dormant New Relic gem (can be enabled via environment variables) - - Expire Rack sessions after 1 week - - Cleaner signin/signup pages - - Improved comments UI - - Better search with filtering, pagination etc - - Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov) - - Prevent project stars duplication when fork project - - Use the default Unicorn socket backlog value of 1024 - - Support Unix domain sockets for Redis - - Store session Redis keys in 'session:gitlab:' namespace - - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match - - Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy) - - Keyboard shortcuts for productivity (Robert Schilling) - - API: filter issues by state (Julien Bianchi) - - API: filter issues by labels (Julien Bianchi) - - Add system hook for ssh key changes - - Add blob permalink link (Ciro Santilli) - - Create annotated tags through UI and API (Sean Edge) - - Snippets search (Charles Bushong) - - Comment new push to existing MR - - Add 'ci' to the blacklist of forbidden names - - Improve text filtering on issues page - - Comment & Close button - - Process git push --all much faster - - Don't allow edit of system notes - - Project wiki search (Ralf Seidler) - - Enabled Shibboleth authentication support (Matus Banas) - - Zen mode (fullscreen) for issues/MR/notes (Robert Schilling) - - Add ability to configure webhook timeout via gitlab.yml (Wes Gurney) - - Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media) - - Add Redis socket support to 'rake gitlab:shell:install' - -v 7.2.1 - - Delete orphaned labels during label migration (James Brooks) - - Security: prevent XSS with stricter MIME types for raw repo files - -v 7.2.0 - - Explore page - - Add project stars (Ciro Santilli) - - Log Sidekiq arguments - - Better labels: colors, ability to rename and remove - - Improve the way merge request collects diffs - - Improve compare page for large diffs - - Expose the full commit message via API - - Fix 500 error on repository rename - - Fix bug when MR download patch return invalid diff - - Test gitlab-shell integration - - Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported - - API for labels (Robert Schilling) - - API: ability to set an import url when creating project for specific user - -v 7.1.1 - - Fix cpu usage issue in Firefox - - Fix redirect loop when changing password by new user - - Fix 500 error on new merge request page - -v 7.1.0 - - Remove observers - - Improve MR discussions - - Filter by description on Issues#index page - - Fix bug with namespace select when create new project page - - Show README link after description for non-master members - - Add @all mention for comments - - Dont show reply button if user is not signed in - - Expose more information for issues with webhook - - Add a mention of the merge request into the default merge request commit message - - Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc - - Fix concurrency issue in repository download - - Dont allow repository name start with ? - - Improve email threading (Pierre de La Morinerie) - - Cleaner help page - - Group milestones - - Improved email notifications - - Contributors API (sponsored by Mobbr) - - Fix LDAP TLS authentication (Boris HUISGEN) - - Show VERSION information on project sidebar - - Improve branch removal logic when accept MR - - Fix bug where comment form is spawned inside the Reply button - - Remove Dir.chdir from Satellite#lock for thread-safety - - Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs! - - Show error message in case of timeout in satellite when create MR - - Show first 100 files for huge diff instead of hiding all - - Change default admin email from admin@local.host to admin@example.com - -v 7.0.0 - - The CPU no longer overheats when you hold down the spacebar - - Improve edit file UI - - Add ability to upload group avatar when create - - Protected branch cannot be removed - - Developers can remove normal branches with UI - - Remove branch via API (sponsored by O'Reilly Media) - - Move protected branches page to Project settings area - - Redirect to Files view when create new branch via UI - - Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso) - - Refactor the markdown relative links processing - - Make it easier to implement other CI services for GitLab - - Group masters can create projects in group - - Deprecate ruby 1.9.3 support - - Only masters can rewrite/remove git tags - - Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible - - UI improvements - - Case-insensetive search for issues - - Update to rails 4.1 - - Improve performance of application for projects and groups with a lot of members - - Formally support Ruby 2.1 - - Include Nginx gitlab-ssl config - - Add manual language detection for highlight.js - - Added example.com/:username routing - - Show notice if your profile is public - - UI improvements for mobile devices - - Improve diff rendering performance - - Drag-n-drop for issues and merge requests between states at milestone page - - Fix '0 commits' message for huge repositories on project home page - - Prevent 500 error page when visit commit page from large repo - - Add notice about huge push over http to unicorn config - - File action in satellites uses default 30 seconds timeout instead of old 10 seconds one - - Overall performance improvements - - Skip init script check on omnibus-gitlab - - Be more selective when killing stray Sidekiqs - - Check LDAP user filter during sign-in - - Remove wall feature (no data loss - you can take it from database) - - Dont expose user emails via API unless you are admin - - Detect issues closed by Merge Request description - - Better email subject lines from email on push service (Alex Elman) - - Enable identicon for gravatar be default - -v 6.9.2 - - Revert the commit that broke the LDAP user filter - -v 6.9.1 - - Fix scroll to highlighted line - - Fix the pagination on load for commits page - -v 6.9.0 - - Store Rails cache data in the Redis `cache:gitlab` namespace - - Adjust MySQL limits for existing installations - - Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed) - - Markdown preview or diff during editing via web editor (Evgeniy Sokovikov) - - Give the Rails cache its own Redis namespace - - Add ability to set different ssh host, if different from http/https - - Fix syntax highlighting for code comments blocks - - Improve comments loading logic - - Stop refreshing comments when the tab is hidden - - Improve issue and merge request mobile UI (Drew Blessing) - - Document how to convert a backup to PostgreSQL - - Fix locale bug in backup manager - - Fix can not automerge when MR description is too long - - Fix wiki backup skip bug - - Two Step MR creation process - - Remove unwanted files from satellite working directory with git clean -fdx - - Accept merge request via API (sponsored by O'Reilly Media) - - Add more access checks during API calls - - Block SSH access for 'disabled' Active Directory users - - Labels for merge requests (Drew Blessing) - - Threaded emails by setting a Message-ID (Philip Blatter) - -v 6.8.0 - - Ability to at mention users that are participating in issue and merge req. discussion - - Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu) - - Make user search case-insensitive (Christopher Arnold) - - Remove omniauth-ldap nickname bug workaround - - Drop all tables before restoring a Postgres backup - - Make the repository downloads path configurable - - Create branches via API (sponsored by O'Reilly Media) - - Changed permission of gitlab-satellites directory not to be world accessible - - Protected branch does not allow force push - - Fix popen bug in `rake gitlab:satellites:create` - - Disable connection reaping for MySQL - - Allow oauth signup without email for twitter and github - - Fix faulty namespace names that caused 500 on user creation - - Option to disable standard login - - Clean old created archives from repository downloads directory - - Fix download link for huge MR diffs - - Expose event and mergerequest timestamps in API - - Fix emails on push service when only one commit is pushed - -v 6.7.3 - - Fix the merge notification email not being sent (Pierre de La Morinerie) - - Drop all tables before restoring a Postgres backup - - Remove yanked modernizr gem - -v 6.7.2 - - Fix upgrader script - -v 6.7.1 - - Fix GitLab CI integration - -v 6.7.0 - - Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations - - Add support for Gemnasium as a Project Service (Olivier Gonzalez) - - Add edit file button to MergeRequest diff - - Public groups (Jason Hollingsworth) - - Cleaner headers in Notification Emails (Pierre de La Morinerie) - - Blob and tree gfm links to anchors work - - Piwik Integration (Sebastian Winkler) - - Show contribution guide link for new issue form (Jeroen van Baarsen) - - Fix CI status for merge requests from fork - - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) - - New page load indicator that includes a spinner that scrolls with the page - - Converted all the help sections into markdown - - LDAP user filters - - Streamline the content of notification emails (Pierre de La Morinerie) - - Fixes a bug with group member administration (Matt DeTullio) - - Sort tag names using VersionSorter (Robert Speicher) - - Add GFM autocompletion for MergeRequests (Robert Speicher) - - Add webhook when a new tag is pushed (Jeroen van Baarsen) - - Add button for toggling inline comments in diff view - - Add retry feature for repository import - - Reuse the GitLab LDAP connection within each request - - Changed markdown new line behaviour to conform to markdown standards - - Fix global search - - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) - - Create and Update MR calls now support the description parameter (Greg Messner) - - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository - - Added Slack service integration (Federico Ravasio) - - Better API responses for access_levels (sponsored by O'Reilly Media) - - Requires at least 2 unicorn workers - - Requires gitlab-shell v1.9+ - - Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License) - - Fix `/:username.keys` response content type (Dmitry Medvinsky) - -v 6.6.5 - - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) - - Hide mr close button for comment form if merge request was closed or inline comment - - Adds ability to reopen closed merge request - -v 6.6.4 - - Add missing html escape for highlighted code blocks in comments, issues - -v 6.6.3 - - Fix 500 error when edit yourself from admin area - - Hide private groups for public profiles - -v 6.6.2 - - Fix 500 error on branch/tag create or remove via UI - -v 6.6.1 - - Fix 500 error on files tab if submodules presents - -v 6.6.0 - - Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys - - Permissions: Developer now can manage issue tracker (modify any issue) - - Improve Code Compare page performance - - Group avatar - - Pygments.rb replaced with highlight.js - - Improve Merge request diff store logic - - Improve render performnace for MR show page - - Fixed Assembla hardcoded project name - - Jira integration documentation - - Refactored app/services - - Remove snippet expiration - - Mobile UI improvements (Drew Blessing) - - Fix block/remove UI for admin::users#show page - - Show users' group membership on users' activity page (Robert Djurasaj) - - User pages are visible without login if user is authorized to a public project - - Markdown rendered headers have id derived from their name and link to their id - - Improve application to work faster with large groups (100+ members) - - Multiple emails per user - - Show last commit for file when view file source - - Restyle Issue#show page and MR#show page - - Ability to filter by multiple labels for Issues page - - Rails version to 4.0.3 - - Fixed attachment identifier displaying underneath note text (Jason Blanchard) - -v 6.5.1 - - Fix branch selectbox when create merge request from fork - -v 6.5.0 - - Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard) - - Add color custimization and previewing to broadcast messages - - Fixed notes anchors - - Load new comments in issues dynamically - - Added sort options to Public page - - New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media) - - Add project visibility icons to dashboard - - Enable secure cookies if https used - - Protect users/confirmation with rack_attack - - Default HTTP headers to protect against MIME-sniffing, force https if enabled - - Bootstrap 3 with responsive UI - - New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth) - - Restyled accept widgets for MR - - SCSS refactored - - Use jquery timeago plugin - - Fix 500 error for rdoc files - - Ability to customize merge commit message (sponsored by Say Media) - - Search autocomplete via ajax - - Add website url to user profile - - Files API supports base64 encoded content (sponsored by O'Reilly Media) - - Added support for Go's repository retrieval (Bruno Albuquerque) - -v 6.4.3 - - Don't use unicorn worker killer if PhusionPassenger is defined - -v 6.4.2 - - Fixed wrong behaviour of script/upgrade.rb - -v 6.4.1 - - Fixed bug with repository rename - - Fixed bug with project transfer - -v 6.4.0 - - Added sorting to project issues page (Jason Blanchard) - - Assembla integration (Carlos Paramio) - - Fixed another 500 error with submodules - - UI: More compact issues page - - Minimal password length increased to 8 symbols - - Side-by-side diff view (Steven Thonus) - - Internal projects (Jason Hollingsworth) - - Allow removal of avatar (Drew Blessing) - - Project webhooks now support issues and merge request events - - Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth) - - Expire event cache on avatar creation/removal (Drew Blessing) - - Archiving old projects (Steven Thonus) - - Rails 4 - - Add time ago tooltips to show actual date/time - - UI: Fixed UI for admin system hooks - - Ruby script for easier GitLab upgrade - - Do not remove Merge requests if fork project was removed - - Improve sign-in/signup UX - - Add resend confirmation link to sign-in page - - Set noreply@HOSTNAME for reply_to field in all emails - - Show GitLab API version on Admin#dashboard - - API Cross-origin resource sharing - - Show READMe link at project home page - - Show repo size for projects in Admin area - -v 6.3.0 - - API for adding gitlab-ci service - - Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey) - - Restyle project home page - - Grammar fixes - - Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev) - - Security improvements - - Added support for GitLab CI 4.0 - - Fixed issue with 500 error when group did not exist - - Ability to leave project - - You can create file in repo using UI - - You can remove file from repo using UI - - API: dropped default_branch attribute from project during creation - - Project default_branch is not stored in db any more. It takes from repo now. - - Admin broadcast messages - - UI improvements - - Dont show last push widget if user removed this branch - - Fix 500 error for repos with newline in file name - - Extended html titles - - API: create/update/delete repo files - - Admin can transfer project to any namespace - - API: projects/all for admin users - - Fix recent branches order - -v 6.2.4 - - Security: Cast API private_token to string (CVE-2013-4580) - - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583) - - Fix for Git SSH access for LDAP users - -v 6.2.3 - - Security: More protection against CVE-2013-4489 - - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546) - - Fix sidekiq rake tasks - -v 6.2.2 - - Security: Update gitlab_git (CVE-2013-4489) - -v 6.2.1 - - Security: Fix issue with generated passwords for new users - -v 6.2.0 - - Public project pages are now visible to everyone (files, issues, wik, etc.) - THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE - - Add group access to permissions page - - Require current password to change one - - Group owner or admin can remove other group owners - - Remove group transfer since we have multiple owners - - Respect authorization in Repository API - - Improve UI for Project#files page - - Add more security specs - - Added search for projects by name to api (Izaak Alpert) - - Make default user theme configurable (Izaak Alpert) - - Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev) - - Rake tasks for webhooks management (Jonhnny Weslley) - - Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov) - - API: Remove group - - API: Remove project - - Avatar upload on profile page with a maximum of 100KB (Steven Thonus) - - Store the sessions in Redis instead of the cookie store - - Fixed relative links in markdown - - User must confirm their email if signup enabled - - User must confirm changed email - -v 6.1.0 - - Project specific IDs for issues, mr, milestones - Above items will get a new id and for example all bookmarked issue urls will change. - Old issue urls are redirected to the new one if the issue id is too high for an internal id. - - Description field added to Merge Request - - API: Sudo api calls (Izaak Alpert) - - API: Group membership api (Izaak Alpert) - - Improved commit diff - - Improved large commit handling (Boyan Tabakov) - - Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey) - - Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson) - - Close issues automatically when pushing commits with a special message - - Improve user removal from admin area - - Invalidate events cache when project was moved - - Remove deprecated classes and rake tasks - - Add event filter for group and project show pages - - Add links to create branch/tag from project home page - - Add public-project? checkbox to new-project view - - Improved compare page. Added link to proceed into Merge Request - - Send an email to a user when they are added to group - - New landing page when you have 0 projects - -v 6.0.0 - - Feature: Replace teams with group membership - We introduce group membership in 6.0 as a replacement for teams. - The old combination of groups and teams was confusing for a lot of people. - And when the members of a team where changed this wasn't reflected in the project permissions. - In GitLab 6.0 you will be able to add members to a group with a permission level for each member. - These group members will have access to the projects in that group. - Any changes to group members will immediately be reflected in the project permissions. - You can even have multiple owners for a group, greatly simplifying administration. - - Feature: Ability to have multiple owners for group - - Feature: Merge Requests between fork and project (Izaak Alpert) - - Feature: Generate fingerprint for ssh keys - - Feature: Ability to create and remove branches with UI - - Feature: Ability to create and remove git tags with UI - - Feature: Groups page in profile. You can leave group there - - API: Allow login with LDAP credentials - - Redesign: project settings navigation - - Redesign: snippets area - - Redesign: ssh keys page - - Redesign: buttons, blocks and other ui elements - - Add comment title to rss feed - - You can use arrows to navigate at tree view - - Add project filter on dashboard - - Cache project graph - - Drop support of root namespaces - - Default theme is classic now - - Cache result of methods like authorize_projects, project.team.members etc - - Remove $.ready events - - Fix onclick events being double binded - - Add notification level to group membership - - Move all project controllers/views under Projects:: module - - Move all profile controllers/views under Profiles:: module - - Apply user project limit only for personal projects - - Unicorn is default web server again - - Store satellites lock files inside satellites dir - - Disabled threadsafety mode in rails - - Fixed bug with loosing MR comments - - Improved MR comments logic - - Render readme file for projects in public area - -v 5.4.2 - - Security: Cast API private_token to string (CVE-2013-4580) - - Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583) - -v 5.4.1 - - Security: Fixes for CVE-2013-4489 - - Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546) - -v 5.4.0 - - Ability to edit own comments - - Documentation improvements - - Improve dashboard projects page - - Fixed nav for empty repos - - GitLab Markdown help page - - Misspelling fixes - - Added support of unicorn and fog gems - - Added client list to API doc - - Fix PostgreSQL database restoration problem - - Increase snippet content column size - - allow project import via git:// url - - Show participants on issues, including mentions - - Notify mentioned users with email - -v 5.3.0 - - Refactored services - - Campfire service added - - HipChat service added - - Fixed bug with LDAP + git over http - - Fixed bug with google analytics code being ignored - - Improve sign-in page if ldap enabled - - Respect newlines in wall messages - - Generate the Rails secret token on first run - - Rename repo feature - - Init.d: remove gitlab.socket on service start - - Api: added teams api - - Api: Prevent blob content being escaped - - Api: Smart deploy key add behaviour - - Api: projects/owned.json return user owned project - - Fix bug with team assignation on project from #4109 - - Advanced snippets: public/private, project/personal (Andrew Kulakov) - - Repository Graphs (Karlo Nicholas T. Soriano) - - Fix dashboard lost if comment on commit - - Update gitlab-grack. Fixes issue with --depth option - - Fix project events duplicate on project page - - Fix postgres error when displaying network graph. - - Fix dashboard event filter when navigate via turbolinks - - init.d: Ensure socket is removed before starting service - - Admin area: Style teams:index, group:show pages - - Own page for failed forking - - Scrum view for milestone - -v 5.2.0 - - Turbolinks - - Git over http with ldap credentials - - Diff with better colors and some spacing on the corners - - Default values for project features - - Fixed huge_commit view - - Restyle project clone panel - - Move Gitlab::Git code to gitlab_git gem - - Move update docs in repo - - Requires gitlab-shell v1.4.0 - - Fixed submodules listing under file tab - - Fork feature (Angus MacArthur) - - git version check in gitlab:check - - Shared deploy keys feature - - Ability to generate default labels set for issues - - Improve gfm autocomplete (Harold Luo) - - Added support for Google Analytics - - Code search feature (Javier Castro) - -v 5.1.0 - - You can login with email or username now - - Corrected project transfer rollback when repository cannot be moved - - Move both repo and wiki when project transfer requested - - Admin area: project editing was removed from admin namespace - - Access: admin user has now access to any project. - - Notification settings - - Gitlab::Git set of objects to abstract from grit library - - Replace Unicorn web server with Puma - - Backup/Restore refactored. Backup dump project wiki too now - - Restyled Issues list. Show milestone version in issue row - - Restyled Merge Request list - - Backup now dump/restore uploads - - Improved performance of dashboard (Andrew Kumanyaev) - - File history now tracks renames (Akzhan Abdulin) - - Drop wiki migration tools - - Drop sqlite migration tools - - project tagging - - Paginate users in API - - Restyled network graph (Hiroyuki Sato) - -v 5.0.1 - - Fixed issue with gitlab-grit being overridden by grit - -v 5.0.0 - - Replaced gitolite with gitlab-shell - - Removed gitolite-related libraries - - State machine added - - Setup gitlab as git user - - Internal API - - Show team tab for empty projects - - Import repository feature - - Updated rails - - Use lambda for scopes - - Redesign admin area -> users - - Redesign admin area -> user - - Secure link to file attachments - - Add validations for Group and Team names - - Restyle team page for project - - Update capybara, rspec-rails, poltergeist to recent versions - - Wiki on git using Gollum - - Added Solarized Dark theme for code review - - Don't show user emails in autocomplete lists, profile pages - - Added settings tab for group, team, project - - Replace user popup with icons in header - - Handle project moving with gitlab-shell - - Added select2-rails for selectboxes with ajax data load - - Fixed search field on projects page - - Added teams to search autocomplete - - Move groups and teams on dashboard sidebar to sub-tabs - - API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell) - - Redesign wall to be more like chat - - Snippets, Wall features are disabled by default for new projects - -v 4.2.0 - - Teams - - User show page. Via /u/username - - Show help contents on pages for better navigation - - Async gitolite calls - - added satellites logs - - can_create_group, can_create_team booleans for User - - Process webhooks async - - GFM: Fix images escaped inside links - - Network graph improved - - Switchable branches for network graph - - API: Groups - - Fixed project download - -v 4.1.0 - - Optional Sign-Up - - Discussions - - Satellites outside of tmp - - Line numbers for blame - - Project public mode - - Public area with unauthorized access - - Load dashboard events with ajax - - remember dashboard filter in cookies - - replace resque with sidekiq - - fix routing issues - - cleanup rake tasks - - fix backup/restore - - scss cleanup - - show preview for note images - - improved network-graph - - get rid of app/roles/ - - added new classes Team, Repository - - Reduce amount of gitolite calls - - Ability to add user in all group projects - - remove deprecated configs - - replaced Korolev font with open font - - restyled admin/dashboard page - - restyled admin/projects page - -v 4.0.0 - - Remove project code and path from API. Use id instead - - Return valid cloneable url to repo for webhook - - Fixed backup issue - - Reorganized settings - - Fixed commits compare - - Refactored scss - - Improve status checks - - Validates presence of User#name - - Fixed postgres support - - Removed sqlite support - - Modified post-receive hook - - Milestones can be closed now - - Show comment events on dashboard - - Quick add team members via group#people page - - [API] expose created date for hooks and SSH keys - - [API] list, create issue notes - - [API] list, create snippet notes - - [API] list, create wall notes - - Remove project code - use path instead - - added username field to user - - rake task to fill usernames based on emails create namespaces for users - - STI Group < Namespace - - Project has namespace_id - - Projects with namespaces also namespaced in gitolite and stored in subdir - - Moving project to group will move it under group namespace - - Ability to move project from namespaces to another - - Fixes commit patches getting escaped (see #2036) - - Support diff and patch generation for commits and merge request - - MergeReqest doesn't generate a temporary file for the patch any more - - Update the UI to allow downloading Patch or Diff - -v 3.1.0 - - Updated gems - - Services: Gitlab CI integration - - Events filter on dashboard - - Own namespace for redis/resque - - Optimized commit diff views - - add alphabetical order for projects admin page - - Improved web editor - - Commit stats page - - Documentation split and cleanup - - Link to commit authors everywhere - - Restyled milestones list - - added Milestone to Merge Request - - Restyled Top panel - - Refactored Satellite Code - - Added file line links - - moved from capybara-webkit to poltergeist + phantomjs - -v 3.0.3 - - Fixed bug with issues list in Chrome - - New Feature: Import team from another project - -v 3.0.2 - - Fixed gitlab:app:setup - - Fixed application error on empty project in admin area - - Restyled last push widget - -v 3.0.1 - - Fixed git over http - -v 3.0.0 - - Projects groups - - Web Editor - - Fixed bug with gitolite keys - - UI improved - - Increased performance of application - - Show user avatar in last commit when browsing Files - - Refactored Gitlab::Merge - - Use Font Awesome for icons - - Separate observing of Note and MergeRequests - - Milestone "All Issues" filter - - Fix issue close and reopen button text and styles - - Fix forward/back while browsing Tree hierarchy - - Show number of notes for commits and merge requests - - Added support pg from box and update installation doc - - Reject ssh keys that break gitolite - - [API] list one project hook - - [API] edit project hook - - [API] list project snippets - - [API] allow to authorize using private token in HTTP header - - [API] add user creation - -v 2.9.1 - - Fixed resque custom config init - -v 2.9.0 - - fixed inline notes bugs - - refactored rspecs - - refactored gitolite backend - - added factory_girl - - restyled projects list on dashboard - - ssh keys validation to prevent gitolite crash - - send notifications if changed permission in project - - scss refactoring. gitlab_bootstrap/ dir - - fix git push http body bigger than 112k problem - - list of labels page under issues tab - - API for milestones, keys - - restyled buttons - - OAuth - - Comment order changed - -v 2.8.1 - - ability to disable gravatars - - improved MR diff logic - - ssh key help page - -v 2.8.0 - - Gitlab Flavored Markdown - - Bulk issues update - - Issues API - - Cucumber coverage increased - - Post-receive files fixed - - UI improved - - Application cleanup - - more cucumber - - capybara-webkit + headless - -v 2.7.0 - - Issue Labels - - Inline diff - - Git HTTP - - API - - UI improved - - System hooks - - UI improved - - Dashboard events endless scroll - - Source performance increased - -v 2.6.0 - - UI polished - - Improved network graph + keyboard nav - - Handle huge commits - - Last Push widget - - Bugfix - - Better performance - - Email in resque - - Increased test coverage - - Ability to remove branch with MR accept - - a lot of code refactored - -v 2.5.0 - - UI polished - - Git blame for file - - Bugfix - - Email in resque - - Better test coverage - -v 2.4.0 - - Admin area stats page - - Ability to block user - - Simplified dashboard area - - Improved admin area - - Bootstrap 2.0 - - Responsive layout - - Big commits handling - - Performance improved - - Milestones - -v 2.3.1 - - Issues pagination - - ssl fixes - - Merge Request pagination - -v 2.3.0 - - Dashboard r1 - - Search r1 - - Project page - - Close merge request on push - - Persist MR diff after merge - - mysql support - - Documentation - -v 2.2.0 - - We’ve added support of LDAP auth - - Improved permission logic (4 roles system) - - Protected branches (now only masters can push to protected branches) - - Usability improved - - twitter bootstrap integrated - - compare view between commits - - wiki feature - - now you can enable/disable issues, wiki, wall features per project - - security fixes - - improved code browsing (ajax branch switch etc) - - improved per-line commenting - - git submodules displayed - - moved to rails 3.2 - - help section improved - -v 2.1.0 - - Project tab r1 - - List branches/tags - - per line comments - - mass user import - -v 2.0.0 - - gitolite as main git host system - - merge requests - - project/repo access - - link to commit/issue feed - - design tab - - improved email notifications - - restyled dashboard - - bugfix - -v 1.2.2 - - common config file gitlab.yml - - issues restyle - - snippets restyle - - clickable news feed header on dashboard - - bugfix - -v 1.2.1 - - bugfix - -v 1.2.0 - - new design - - user dashboard - - network graph - - markdown support for comments - - encoding issues - - wall like twitter timeline - -v 1.1.0 - - project dashboard - - wall redesigned - - feature: code snippets - - fixed horizontal scroll on file preview - - fixed app crash if commit message has invalid chars - - bugfix & code cleaning - -v 1.0.2 - - fixed bug with empty project - - added adv validation for project path & code - - feature: issues can be sortable - - bugfix - - username displayed on top panel - -v 1.0.1 - - fixed: with invalid source code for commit - - fixed: lose branch/tag selection when use tree navigation - - when history clicked - display path - - bug fix & code cleaning - -v 1.0.0 - - bug fix - - projects preview mode - -v 0.9.6 - - css fix - - new repo empty tree until restart server - fixed - -v 0.9.4 - - security improved - - authorization improved - - html escaping - - bug fix - - increased test coverage - - design improvements - -v 0.9.1 - - increased test coverage - - design improvements - - new issue email notification - - updated app name - - issue redesigned - - issue can be edit - -v 0.8.0 - - syntax highlight for main file types - - redesign - - stability - - security fixes - - increased test coverage - - email notification +v 7.14.3 through 0.8.0 + - See changelogs/archive.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8093a61b4c..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 @@ -340,6 +287,8 @@ request is as follows: migrations on a fresh database before the MR is reviewed. If the review leads to large changes in the MR, do this again once the review is complete. 1. For more complex migrations, write tests. +1. Merge requests **must** adhere to the [merge request performance + guidelines](doc/development/merge_request_performance_guidelines.md). The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. This is the best time to submit an MR and get diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index b4d6d12101f..6f4eebdf6f6 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.11 +0.8.1 @@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.4.7' +gem 'gitlab_git', '~> 10.6.3' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -97,9 +97,6 @@ gem 'fog-rackspace', '~> 0.1.1' # for aws storage gem 'unf', '~> 0.1.4' -# Authorization -gem 'six', '~> 0.2.0' - # Seed data gem 'seed-fu', '~> 2.3.5' @@ -349,5 +346,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..c421713f6a1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) - gitlab_git (10.4.7) + gitlab_git (10.6.3) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -584,7 +584,7 @@ GEM railties (>= 4.2.0, < 5.1) rinku (2.0.0) rotp (2.1.2) - rouge (2.0.5) + rouge (2.0.6) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -683,7 +683,6 @@ GEM rack (~> 1.5) rack-protection (~> 1.4) tilt (>= 1.3, < 3) - six (0.2.0) slack-notifier (1.2.1) slop (3.6.0) spinach (0.8.10) @@ -772,7 +771,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) @@ -859,7 +858,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) github-markup (~> 1.4) gitlab-flowdock-git-hook (~> 1.0.1) - gitlab_git (~> 10.4.7) + gitlab_git (~> 10.6.3) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) @@ -954,7 +953,6 @@ DEPENDENCIES sidekiq-cron (~> 0.4.0) simplecov (= 0.12.0) sinatra (~> 1.4.4) - six (~> 0.2.0) slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) @@ -980,7 +978,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/PROCESS.md b/PROCESS.md index 8e1a3f7360f..8af660fbdd1 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -50,7 +50,7 @@ etc.). The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help -on those issue. Please select someone with relevant experience from +on those issues. Please select someone with relevant experience from [GitLab core team][core-team]. If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png Binary files differdeleted file mode 100644 index 5b55e12571c..00000000000 --- a/app/assets/images/icon-link.png +++ /dev/null diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg new file mode 100644 index 00000000000..7e242586bad --- /dev/null +++ b/app/assets/images/icon_anchor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js index 151455ce4a3..d4a4c7abaa1 100644 --- a/app/assets/javascripts/LabelManager.js +++ b/app/assets/javascripts/LabelManager.js @@ -3,6 +3,7 @@ LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time'; function LabelManager(opts) { + // Defaults var ref, ref1, ref2; if (opts == null) { opts = {}; @@ -28,6 +29,7 @@ $btn = $(e.currentTarget); $label = $("#" + ($btn.data('domId'))); action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + // Make sure tooltip will hide $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby'))); $tooltip.tooltip('destroy'); return _this.toggleLabelPriority($label, action); @@ -42,6 +44,7 @@ url = $label.find('.js-toggle-priority').data('url'); $target = this.prioritizedLabels; $from = this.otherLabels; + // Optimistic update if (action === 'remove') { $target = this.otherLabels; $from = this.prioritizedLabels; @@ -53,6 +56,7 @@ $target.find('.empty-message').addClass('hidden'); } $label.detach().appendTo($target); + // Return if we are not persisting state if (!persistState) { return; } @@ -61,6 +65,7 @@ url: url, type: 'DELETE' }); + // Restore empty message if (!$from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } 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..d5e11e22be5 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -12,7 +12,7 @@ } Activities.prototype.updateTooltips = function() { - return gl.utils.localTimeAgo($('.js-timeago', '#activity')); + return gl.utils.localTimeAgo($('.js-timeago', '.content_list')); }; Activities.prototype.reloadActivities = function() { @@ -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/api.js b/app/assets/javascripts/api.js index 84b292e59c6..6df2ecf57a2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,8 @@ return callback(group); }); }, + // Return groups list. Filtered by query + // Only active groups retrieved groups: function(query, skip_ldap, callback) { var url = Api.buildUrl(Api.groupsPath); return $.ajax({ @@ -38,6 +40,7 @@ return callback(groups); }); }, + // Return namespaces list. Filtered by query namespaces: function(query, callback) { var url = Api.buildUrl(Api.namespacesPath); return $.ajax({ @@ -52,6 +55,7 @@ return callback(namespaces); }); }, + // Return projects list. Filtered by query projects: function(query, order, callback) { var url = Api.buildUrl(Api.projectsPath); return $.ajax({ @@ -82,6 +86,7 @@ return callback(message.responseJSON); }); }, + // Return group projects list. Filtered by query groupProjects: function(group_id, query, callback) { var url = Api.buildUrl(Api.groupProjectsPath) .replace(':id', group_id); @@ -97,6 +102,7 @@ return callback(projects); }); }, + // Return text for a specific license licenseText: function(key, data, callback) { var url = Api.buildUrl(Api.licensePath) .replace(':key', key); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index fc354dfd677..31fa508d6c1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,3 +1,9 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require jquery2 */ /*= require jquery-ui/autocomplete */ /*= require jquery-ui/datepicker */ @@ -76,6 +82,7 @@ } }; + // Disable button if text field is empty window.disableButtonIfEmptyField = function(field_selector, button_selector) { var closest_submit, field; field = $(field_selector); @@ -92,6 +99,7 @@ }); }; + // Disable button if any input field with given selector is empty window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) { var closest_submit, updateButtons; closest_submit = form.find(button_selector); @@ -128,6 +136,8 @@ window.addEventListener("hashchange", shiftWindow); window.onload = function() { + // Scroll the window to avoid the topnav bar + // https://github.com/twitter/bootstrap/issues/1768 if (location.hash) { return setTimeout(shiftWindow, 100); } @@ -149,6 +159,8 @@ return $(this).select().one('mouseup', function(e) { return e.preventDefault(); }); + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input }); $('.remove-row').bind('ajax:success', function() { $(this).tooltip('destroy') @@ -163,6 +175,7 @@ }); $('select.select2').select2({ width: 'resolve', + // Initialize select2 selects dropdownAutoWidth: true }); $('.js-select2').bind('select2-close', function() { @@ -170,25 +183,28 @@ $('.select2-container-active').removeClass('select2-container-active'); return $(':focus').blur(); }), 1); + // Close select2 on escape }); + // Initialize tooltips $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', placement: function(_, el) { - var $el; - $el = $(el); - return $el.data('placement') || 'bottom'; + return $(el).data('placement') || 'bottom'; } }); $('.trigger-submit').on('change', function() { return $(this).parents('form').submit(); + // Form submitter }); gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Flash if ((flash = $(".flash-container")).length > 0) { flash.click(function() { return $(this).fadeOut(); }); flash.show(); } + // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) { var buttons; buttons = $('[type="submit"]', this); @@ -209,6 +225,7 @@ } }); $('.account-box').hover(function() { + // Show/Hide the profile menu when hovering the account box return $(this).toggleClass('hover'); }); $document.on('click', '.diff-content .js-show-suppressed-diff', function() { @@ -216,6 +233,7 @@ $container = $(this).parent(); $container.next('table').show(); return $container.remove(); + // Commit show suppressed diff }); $('.navbar-toggle').on('click', function() { $('.header-content .title').toggle(); @@ -223,6 +241,7 @@ $('.header-content .navbar-collapse').toggle(); return $('.navbar-toggle').toggleClass('active'); }); + // Show/hide comments on diff $body.on("click", ".js-toggle-diff-comments", function(e) { var $this = $(this); $this.toggleClass('active'); @@ -286,42 +305,9 @@ gl.awardsHandler = new AwardsHandler(); checkInitialSidebarSize(); new Aside(); - if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') { - $.cookie('pin_nav', 'false', { - path: '/', - expires: 365 * 10 - }); - $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned'); - $('.navbar-fixed-top').removeClass('header-pinned-nav'); - } - $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) { - var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText; - e.preventDefault(); - $pinBtn = $(e.currentTarget); - $page = $('.page-with-sidebar'); - $topNav = $('.navbar-fixed-top'); - $tooltip = $("#" + ($pinBtn.attr('aria-describedby'))); - doPinNav = !$page.is('.page-sidebar-pinned'); - tooltipText = 'Pin navigation'; - $(this).toggleClass('is-active'); - if (doPinNav) { - $page.addClass('page-sidebar-pinned'); - $topNav.addClass('header-pinned-nav'); - } else { - $tooltip.remove(); - $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); - $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded'); - } - $.cookie('pin_nav', doPinNav, { - path: '/', - expires: 365 * 10 - }); - if ($.cookie('pin_nav') === 'true' || doPinNav) { - tooltipText = 'Unpin navigation'; - } - $tooltip.find('.tooltip-inner').text(tooltipText); - return $pinBtn.attr('title', tooltipText).tooltip('fixTitle'); - }); + + // bind sidebar events + new gl.Sidebar(); // Custom time ago gl.utils.shortTimeAgo($('.js-short-timeago')); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 7116512d6b7..a9aec6e8ea4 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -16,7 +16,7 @@ } Autosave.prototype.restore = function() { - var e, error, text; + var e, text; if (window.localStorage == null) { return; } @@ -41,7 +41,7 @@ if ((text != null ? text.length : void 0) > 0) { try { return window.localStorage.setItem(this.key, text); - } catch (undefined) {} + } catch (error) {} } else { return this.reset(); } @@ -53,7 +53,7 @@ } try { return window.localStorage.removeItem(this.key); - } catch (undefined) {} + } catch (error) {} }; return Autosave; diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index aee1c29eee3..0decc6d09e6 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -86,6 +86,8 @@ AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { var css, position; position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body css = { top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" }; @@ -255,12 +257,12 @@ }; AwardsHandler.prototype.animateEmoji = function($emoji) { - var className; - className = 'pulse animated'; + var className = 'pulse animated once short'; $emoji.addClass(className); - return setTimeout((function() { - return $emoji.removeClass(className); - }), 321); + + $emoji.on('webkitAnimationEnd animationEnd', function() { + $(this).removeClass(className); + }); }; AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { @@ -284,6 +286,7 @@ if (emojiIcon.length > 0) { unicodeName = emojiIcon.data('unicode-name'); } else { + // Find by alias unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); } return "emoji-" + unicodeName; @@ -320,6 +323,7 @@ frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis.push(emoji); return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { + path: gon.relative_url_root || '/', expires: 365 }); }; @@ -349,8 +353,10 @@ return function(ev) { var found_emojis, h5, term, ul; term = $(ev.target).val(); + // Clean previous search results $('ul.emoji-menu-search, h5.emoji-search').remove(); if (term) { + // Generate a search result block h5 = $('<h5>').text('Search results'); found_emojis = _this.searchEmojis(term).show(); ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index f977a1e8a7b..dc8ae601961 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,7 +1,5 @@ /*= require jquery.ba-resize */ - - /*= require autosize */ (function() { diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 3631d1b74ac..1df681a4816 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -5,6 +5,12 @@ container = $(this).closest(".js-details-container"); return container.toggleClass("open"); }); + // Show details content. Hides link after click. + // + // %div + // %a.js-details-expand + // %div.js-details-content + // return $("body").on("click", ".js-details-expand", function(e) { $(this).next('.js-details-content').removeClass("hide"); $(this).hide(); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 3527d0a95fc..54b7360ab41 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,6 +1,20 @@ - +// Quick Submit behavior +// +// When a child field of a form with a `js-quick-submit` class receives a +// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form +// is submitted. +// /*= require extensions/jquery */ +// +// ### Example Markup +// +// <form action="/foo" class="js-quick-submit"> +// <input type="text" /> +// <textarea></textarea> +// <input type="submit" value="Submit" /> +// </form> +// (function() { var isMac, keyCodeIs; @@ -17,6 +31,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { var $form, $submit_button; + // Enter if (!keyCodeIs(e, 13)) { return; } @@ -33,8 +48,11 @@ return $form.submit(); }); + // If the user tabs to a submit button on a `js-quick-submit` form, display a + // tooltip to let them know they could've used the hotkey $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) { var $this, title; + // Tab if (!keyCodeIs(e, 9)) { return; } diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index db0b36b24e9..894034bdd54 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,6 +1,18 @@ - +// Requires Input behavior +// +// When called on a form with input fields with the `required` attribute, the +// form's submit button will be disabled until all required fields have values. +// /*= require extensions/jquery */ +// +// ### Example Markup +// +// <form class="js-requires-input"> +// <input type="text" required="required"> +// <input type="submit" value="Submit"> +// </form> +// (function() { $.fn.requiresInput = function() { var $button, $form, fieldSelector, requireInput, required; @@ -11,14 +23,17 @@ requireInput = function() { var values; values = _.map($(fieldSelector, $form), function(field) { + // Collect the input values of *all* required fields return field.value; }); + // Disable the button if any required fields are empty if (values.length && _.any(values, _.isEmpty)) { return $button.disable(); } else { return $button.enable(); } }; + // Set initial button state requireInput(); return $form.on('change input', fieldSelector, requireInput); }; @@ -27,6 +42,8 @@ var $form, hideOrShowHelpBlock; $form = $('form.js-requires-input'); $form.requiresInput(); + // Hide or Show the help block when creating a new project + // based on the option selected hideOrShowHelpBlock = function(form) { var selected; selected = $('.js-select-namespace option:selected'); diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 1b7b63489ea..a6ce378d67a 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,10 +1,33 @@ -(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(); + // Toggle button. Show/hide content inside parent container. + // Button does not change visibility. If button has icon - it changes chevron style. + // + // %div.js-toggle-container + // %a.js-toggle-button + // %div.js-toggle-content + // + $('body').on('click', '.js-toggle-button', 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/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index f4044f22db2..8cca1aa9232 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -8,6 +8,8 @@ autoDiscover: false, autoProcessQueue: false, url: form.attr('action'), + // Rails uses a hidden input field for PUT + // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails method: method, clickable: true, uploadMultiple: false, @@ -36,6 +38,7 @@ formData.append('commit_message', form.find('.js-commit-message').val()); }); }, + // Override behavior of adding error underneath preview error: function(file, errorMessage) { var stripped; stripped = $("<div/>").html(errorMessage).text(); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index b0a37ef0e0a..b18b6962382 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -66,6 +66,9 @@ // be added by all subclasses. }; + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { this.editor.setValue(file.content, 1); if (!skipFocus) this.editor.focus(); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 649c79daee8..b846bab0424 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -18,6 +18,8 @@ return function() { return $("#file-content").val(_this.editor.getValue()); }; + // Before a form submission, move the content from the Ace editor into the + // submitted textarea })(this)); this.initModePanesAndLinks(); new BlobLicenseSelectors({ diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index a612cf0f1ae..91c12570e09 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -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 d7f4107cb02..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, diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index a6644e9eb8c..50fc11d7737 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -20,7 +20,8 @@ data () { return { scrollOffset: 250, - filters: Store.state.filters + filters: Store.state.filters, + showCount: false }; }, watch: { @@ -30,6 +31,15 @@ this.$els.list.scrollTop = 0; }, deep: true + }, + issues () { + this.$nextTick(() => { + if (this.scrollHeight() > this.listHeight()) { + this.showCount = true; + } else { + this.showCount = false; + } + }); } }, methods: { @@ -58,6 +68,7 @@ group: 'issues', sort: false, disabled: this.disabled, + filter: '.board-list-count', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index be2b8c568a8..91fd620fdb3 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -11,6 +11,7 @@ class List { this.loading = true; this.loadingMore = false; this.issues = []; + this.issuesSize = 0; if (obj.label) { this.label = new ListLabel(obj.label); @@ -51,17 +52,13 @@ class List { } nextPage () { - if (Math.floor(this.issues.length / 20) === this.page) { + if (this.issuesSize > this.issues.length) { this.page++; return this.getIssues(false); } } - canSearch () { - return this.type === 'backlog'; - } - getIssues (emptyIssues = true) { const filters = this.filters; let data = { page: this.page }; @@ -80,12 +77,13 @@ class List { .then((resp) => { const data = resp.json(); this.loading = false; + this.issuesSize = data.size; if (emptyIssues) { this.issues = []; } - this.createIssues(data); + this.createIssues(data.issues); }); } @@ -96,14 +94,20 @@ class List { } addIssue (issue, listFrom) { - this.issues.push(issue); + if (!this.findIssue(issue.id)) { + this.issues.push(issue); - if (this.label) { - issue.addLabel(this.label); - } + if (this.label) { + issue.addLabel(this.label); + } - if (listFrom) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id); + if (listFrom) { + this.issuesSize++; + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } } } @@ -116,6 +120,7 @@ class List { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { + this.issuesSize--; issue.removeLabel(this.label); } 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/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index f9f9f7999d4..b5ff3a81ed5 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,10 +1,7 @@ -Vue.http.interceptors.push((request, next) => { +Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - Vue.nextTick(() => { - setTimeout(() => { - Vue.activeResources--; - }, 500); + next(function (response) { + Vue.activeResources--; }); - next(); }); diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 1e0148e5798..5fef9725178 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -23,6 +23,7 @@ if ($(allDeviceSelector.join(",")).length) { return; } + // Create all the elements els = $.map(BREAKPOINTS, function(breakpoint) { return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; }); @@ -40,6 +41,7 @@ BreakpointInstance.prototype.getBreakpointSize = function() { var $visibleDevice; $visibleDevice = this.visibleDevice; + // the page refreshed via turbolinks if (!$visibleDevice().length) { this.setup(); } diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0d7d29bb0d0..10abeb50f4b 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -16,6 +16,7 @@ this.toggleSidebar = bind(this.toggleSidebar, this); this.updateDropdown = bind(this.updateDropdown, this); clearInterval(Build.interval); + // Init breakpoint checker this.bp = Breakpoints.get(); $('.js-build-sidebar').niceScroll(); @@ -42,6 +43,9 @@ $(this).data("state", "enabled"); return $(this).text("disable autoscroll"); } + // + // Bind autoscroll button to follow build output + // }); Build.interval = setInterval((function(_this) { return function() { @@ -49,17 +53,23 @@ return _this.getBuildTrace(); } }; + // + // Check for new build output if user still watching build page + // Only valid for runnig build when output changes during time + // })(this), 4000); } } Build.prototype.getInitialBuildTrace = function() { + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] + return $.ajax({ url: this.build_url, dataType: 'json', success: function(build_data) { $('.js-build-output').html(build_data.trace_html); - if (build_data.status === 'success' || build_data.status === 'failed') { + if (removeRefreshStatuses.indexOf(build_data.status) >= 0) { return $('.js-build-refresh').remove(); } } diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 new file mode 100644 index 00000000000..8d3e29794a1 --- /dev/null +++ b/app/assets/javascripts/build_variables.js.es6 @@ -0,0 +1,6 @@ +$(function(){ + $('.reveal-variables').off('click').on('click',function(){ + $('.js-build').toggle().niceScroll(); + $(this).hide(); + }); +}); diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js index c0d0b2d049f..e893491b19b 100644 --- a/app/assets/javascripts/commit/image-file.js +++ b/app/assets/javascripts/commit/image-file.js @@ -2,6 +2,7 @@ this.ImageFile = (function() { var prepareFrames; + // Width where images must fits in, for 2-up this gets divided by 2 ImageFile.availWidth = 900; ImageFile.viewModes = ['two-up', 'swipe']; @@ -9,6 +10,7 @@ function ImageFile(file) { this.file = file; this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) { + // Determine if old and new file has same dimensions, if not show 'two-up' view return function(deletedWidth, deletedHeight) { return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) { if (width === deletedWidth && height === deletedHeight) { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 37f168c5190..9132089adcd 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -45,6 +45,7 @@ CommitsList.content.html(data.html); return history.replaceState({ page: commitsUrl + // Change url so if user reload a page - search results are saved }, document.title, commitsUrl); }, dataType: "json" diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index c43af17442b..3e20db7e308 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -6,14 +6,19 @@ genericSuccess = function(e) { showTooltip(e.trigger, 'Copied!'); + // Clear the selection and blur the trigger so it loses its border e.clearSelection(); return $(e.trigger).blur(); }; + // Safari doesn't support `execCommand`, so instead we inform the user to + // copy manually. + // + // See http://clipboardjs.com/#browser-support genericError = function(e) { var key; if (/Mac/i.test(navigator.userAgent)) { - key = '⌘'; + key = '⌘'; // Command } else { key = 'Ctrl'; } diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 3dd7ceba92f..c8634b78f2b 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -39,6 +39,9 @@ bottom: unfoldBottom, offset: offset, unfold: unfold, + // indent is used to compensate for single space indent to fit + // '+' and '-' prepended to diff lines, + // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 indent: 1, view: file.data('view') }; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ba64d2bcf0b..99b16f7d59b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -23,6 +23,7 @@ case 'projects:boards:show': shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); new IssuableBulkActions(); @@ -93,10 +94,6 @@ break; case "projects:merge_requests:conflicts": window.mcui = new MergeConflictResolver() - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new Activities(); break; @@ -167,6 +164,8 @@ } break; case 'projects:network:show': + // Ensure we don't create a particular shortcut handler here. This is + // already created, where the network graph is created. shortcut_handler = true; break; case 'projects:forks:new': @@ -199,6 +198,7 @@ break; case 'labels': switch (path[2]) { + case 'new': case 'edit': new Labels(); } @@ -262,12 +262,14 @@ shortcut_handler = new ShortcutsNavigation(); } } + // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { return new Shortcuts(); } }; Dispatcher.prototype.initSearch = function() { + // Only when search form is present if ($('.search').length) { return new SearchAutocomplete(); } diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 5a725a41fd1..bf68b7e3a9b 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,6 +2,7 @@ this.DueDateSelect = (function() { function DueDateSelect() { var $datePicker, $dueDate, $loading; + // Milestone edit/new form $datePicker = $('.datepicker'); if ($datePicker.length) { $dueDate = $('#milestone_due_date'); @@ -16,6 +17,7 @@ e.preventDefault(); return $.datepicker._clearDate($datePicker); }); + // Issuable sidebar $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); $('.js-due-date-select').each(function(i, dropdown) { var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL; @@ -38,6 +40,7 @@ }); addDueDate = function(isDropdown) { var data, date, mediumDate, value; + // Create the post date value = $("input[name='" + fieldName + "']").val(); if (value !== '') { date = new Date(value.replace(new RegExp('-', 'g'), ',')); diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index ae3dde63da3..4978e24949c 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -1,3 +1,4 @@ +// Disable an element and add the 'disabled' Bootstrap class (function() { $.fn.extend({ disable: function() { @@ -5,6 +6,7 @@ } }); + // Enable an element and remove the 'disabled' Bootstrap class $.fn.extend({ enable: function() { return $(this).removeAttr('disabled').removeClass('disabled'); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3dca06d36b1..d0786bf0053 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,3 +1,4 @@ +// Creates the variables for setting up GFM auto-completion (function() { if (window.GitLab == null) { window.GitLab = {}; @@ -8,18 +9,22 @@ dataLoaded: false, cachedData: {}, dataSource: '', + // Emoji Emoji: { template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' }, + // Team Members Members: { template: '<li>${username} <small>${title}</small></li>' }, Labels: { template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' }, + // Issues and MergeRequests Issues: { template: '<li><small>${id}</small> ${title}</li>' }, + // Milestones Milestones: { template: '<li>${title}</li>' }, @@ -48,8 +53,11 @@ } }, setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); + // destroy previous instances this.destroyAtWho(); + // set up instances this.setupAtWho(); if (this.dataSource) { if (!this.dataLoading && !this.cachedData) { @@ -63,6 +71,11 @@ return _this.loadData(data); }); }; + // We should wait until initializations are done + // and only trigger the last .setup since + // The previous .dataSource belongs to the previous issuable + // and the last one will have the **proper** .dataSource property + // TODO: Make this a singleton and turn off events when moving to another page })(this), 1000); } if (this.cachedData != null) { @@ -71,6 +84,7 @@ } }, setupAtWho: function() { + // Emoji this.input.atwho({ at: ':', displayTpl: (function(_this) { @@ -90,6 +104,7 @@ beforeInsert: this.DefaultOptions.beforeInsert } }); + // Team Members this.input.atwho({ at: '@', displayTpl: (function(_this) { @@ -321,13 +336,22 @@ loadData: function(data) { this.cachedData = data; this.dataLoaded = true; + // load members this.input.atwho('load', '@', data.members); + // load issues this.input.atwho('load', 'issues', data.issues); + // load milestones this.input.atwho('load', 'milestones', data.milestones); + // load merge requests this.input.atwho('load', 'mergerequests', data.mergerequests); + // load emojis this.input.atwho('load', ':', data.emojis); + // load labels this.input.atwho('load', '~', data.labels); + // load commands this.input.atwho('load', '/', data.commands); + // This trigger at.js again + // otherwise we would be stuck with loading until the user types return $(':focus').trigger('keyup'); } }; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 0179b320a3b..bea141bae51 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -21,12 +21,14 @@ $clearButton = $inputContainer.find('.js-dropdown-input-clear'); this.indeterminateIds = []; $clearButton.on('click', (function(_this) { + // Clear click return function(e) { e.preventDefault(); e.stopPropagation(); return _this.input.val('').trigger('keyup').focus(); }; })(this)); + // Key events timeout = ""; this.input .on('keydown', function (e) { @@ -49,6 +51,7 @@ if (keyCode === 13 && !options.elIsInput) { return false; } + // Only filter asynchronously only if option remote is set if (this.options.remote) { clearTimeout(timeout); return timeout = setTimeout(function() { @@ -79,11 +82,27 @@ if ((data != null) && !this.options.filterByText) { results = data; if (search_text !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] if (_.isArray(data)) { results = fuzzaldrinPlus.filter(data, search_text, { key: this.options.keys }); } else { + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } if (gl.utils.isObject(data)) { results = {}; for (key in data) { @@ -117,7 +136,7 @@ } }); } else { - return elements.show(); + return elements.show().removeClass('option-hidden'); } } }; @@ -140,6 +159,7 @@ this.options.beforeSend(); } return this.dataEndpoint("", (function(_this) { + // Fetch the data by calling the data funcfion return function(data) { if (_this.options.success) { _this.options.success(data); @@ -171,6 +191,7 @@ }; })(this) }); + // Fetch the data through ajax if the data is a string }; return GitLabDropdownRemote; @@ -190,9 +211,9 @@ currentIndex = -1; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden'; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")"; + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; CURSOR_SELECT_SCROLL_PADDING = 5 @@ -209,13 +230,18 @@ self = this; selector = $(this.el).data("target"); this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + // Set Defaults ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true; + // If no input is passed create a default one self = this; + // If selector was passed if (_.isString(this.filterInput)) { this.filterInput = this.getElement(this.filterInput); } searchFields = this.options.search ? this.options.search.fields : []; if (this.options.data) { + // If we provided data + // data could be an array of objects or a group of arrays if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { this.fullData = this.options.data; currentIndex = -1; @@ -232,10 +258,12 @@ return _this.filter.input.trigger('keyup'); } }; + // Remote data })(this) }); } } + // Init filterable if (this.options.filterable) { this.filter = new GitLabDropdownFilter(this.filterInput, { elIsInput: $(this.el).is('input'), @@ -278,12 +306,14 @@ })(this) }); } + // Event listeners this.dropdown.on("shown.bs.dropdown", this.opened); this.dropdown.on("hidden.bs.dropdown", this.hidden); $(this.el).on("update.label", this.updateLabel); this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); this.dropdown.on('keyup', (function(_this) { return function(e) { + // Escape key if (e.which === 27) { return $('.dropdown-menu-close', _this.dropdown).trigger('click'); } @@ -327,6 +357,7 @@ } } + // Finds an element inside wrapper element GitLabDropdown.prototype.getElement = function(selector) { return this.dropdown.find(selector); }; @@ -344,6 +375,7 @@ } } menu.toggleClass(PAGE_TWO_CLASS); + // Focus first visible input on active page return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); }; @@ -351,23 +383,28 @@ var full_html, groupData, html, name; this.renderedData = data; if (this.options.filterable && data.length === 0) { + // render no matching results html = [this.noResults()]; } else { + // Handle array groups if (gl.utils.isObject(data)) { html = []; for (name in data) { groupData = data[name]; html.push(this.renderItem({ header: name + // Add header for each group }, name)); this.renderData(groupData, name).map(function(item) { return html.push(item); }); } } else { + // Render each row html = this.renderData(data); } } + // Render the full menu full_html = this.renderMenu(html); return this.appendMenu(full_html); }; @@ -406,6 +443,7 @@ if (this.options.setActiveIds) { this.options.setActiveIds.call(this); } + // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); } @@ -427,6 +465,8 @@ if (this.options.filterable) { $input.blur().val(""); } + // Triggering 'keyup' will re-render the dropdown which is not always required + // specially if we want to keep the state of the dropdown needed for bulk-assignment if (!this.options.persistWhenHide) { $input.trigger("keyup"); } @@ -439,6 +479,7 @@ return this.dropdown.trigger('hidden.gl.dropdown'); }; + // Render the full menu GitLabDropdown.prototype.renderMenu = function(html) { var menu_html; menu_html = ""; @@ -450,6 +491,7 @@ return menu_html; }; + // Append the menu into the dropdown GitLabDropdown.prototype.appendMenu = function(html) { var selector; selector = '.dropdown-content'; @@ -465,19 +507,24 @@ group = false; } if (index == null) { + // Render the row index = false; } html = ""; + // Divider if (data === "divider") { return "<li class='divider'></li>"; } + // Separator is a full-width divider if (data === "separator") { return "<li class='separator'></li>"; } + // Header if (data.header != null) { return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header }); } if (this.options.renderRow) { + // Call the render function html = this.options.renderRow.call(this.options, data, this); } else { if (!selected) { @@ -489,11 +536,13 @@ selected = true; } } + // Set URL if (this.options.url != null) { url = this.options.url(data); } else { url = data.url != null ? data.url : '#'; } + // Set Text if (this.options.text != null) { text = this.options.text(data); } else { @@ -556,7 +605,7 @@ if (isInput) { field = $(this.el); } else { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + escape(value) + "']"); } if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); @@ -565,10 +614,6 @@ } else { field.remove(); } - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } - return selectedObject; } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); @@ -578,7 +623,6 @@ if (!field.length && fieldName) { 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); @@ -589,10 +633,8 @@ if (value == null) { field.remove(); } + // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); - if (this.options.toggleLabel) { - this.updateLabel(selectedObject, el, this); - } if (value != null) { if (!field.length && fieldName) { this.addInput(fieldName, value, selectedObject); @@ -600,12 +642,19 @@ 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, selectedObject) { var $input; + // Create hidden input for form $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); if (this.options.inputId != null) { $input.attr('id', this.options.inputId); @@ -627,6 +676,7 @@ if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one " + selector; } + // simulate a click on the first link $el = $(selector, this.dropdown); if ($el.length) { var href = $el.attr('href'); @@ -655,11 +705,15 @@ e.stopImmediatePropagation(); PREV_INDEX = currentIndex; $listItems = $(selector, _this.dropdown); + // if @options.filterable + // $input.blur() if (currentKeyCode === 40) { + // Move down if (currentIndex < ($listItems.length - 1)) { currentIndex += 1; } } else if (currentKeyCode === 38) { + // Move up if (currentIndex > 0) { currentIndex -= 1; } @@ -687,24 +741,32 @@ GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + // Remove the class for the previously focused row $('.is-focused', this.dropdown).removeClass('is-focused'); + // Update the class for the row at the specific index $listItem = $listItems.eq(index); $listItem.find('a:first-child').addClass("is-focused"); + // Dropdown content scroll area $dropdownContent = $listItem.closest('.dropdown-content'); dropdownScrollTop = $dropdownContent.scrollTop(); dropdownContentHeight = $dropdownContent.outerHeight(); dropdownContentTop = $dropdownContent.prop('offsetTop'); dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + // Get the offset bottom of the list item listItemHeight = $listItem.outerHeight(); listItemTop = $listItem.prop('offsetTop'); listItemBottom = listItemTop + listItemHeight; if (!index) { + // Scroll the dropdown content to the top $dropdownContent.scrollTop(0) } else if (index === ($listItems.length - 1)) { + // Scroll the dropdown content to the bottom $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + // Scroll the dropdown content down $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + // Scroll the dropdown content up return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); } }; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 528a673eb15..2703adc0705 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,12 +3,15 @@ function GLForm(form) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form this.destroy(); + // Setup the form this.setupForm(); this.form.data('gl-form', this); } GLForm.prototype.destroy = function() { + // Clean form listeners this.clearEventListeners(); return this.form.data('gl-form', null); }; @@ -21,12 +24,15 @@ this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + // remove notify commit author checkbox for non-commit notes GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); + // form and textarea event listeners this.addEventListeners(); gl.text.init(this.form); } + // hide discard button this.form.find('.js-note-discard').hide(); return this.form.show(); }; diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index b95faadc8e7..4886da9f21f 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,7 +1,11 @@ - +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require_tree . */ (function() { - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index a646ca1d84f..7d9d4d7c679 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -204,6 +204,7 @@ function ContributorsAuthorGraph(data1) { this.data = data1; + // Don't split graph size in half for mobile devices. if ($(window).width() < 768) { this.width = $('.content').width() - 80; } else { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index fd5b6dc0ddd..7c2eebcdd44 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -38,6 +38,7 @@ return _this.formatSelection.apply(_this, args); }, dropdownCssClass: "ajax-groups-dropdown", + // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 0f840821f53..9efad1ce943 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -10,21 +10,24 @@ ImporterStatus.prototype.initStatusPage = function() { $('.js-add-to-import').off('click').on('click', (function(_this) { return function(e) { - var $btn, $namespace_input, $target_field, $tr, id, new_namespace; + var $btn, $namespace_input, $target_field, $tr, id, target_namespace; $btn = $(e.currentTarget); $tr = $btn.closest('tr'); $target_field = $tr.find('.import-target'); $namespace_input = $target_field.find('input'); id = $tr.attr('id').replace('repo_', ''); - new_namespace = null; + target_namespace = null; + if ($namespace_input.length > 0) { - new_namespace = $namespace_input.prop('value'); - $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name'))); + target_namespace = $namespace_input.prop('value'); + $target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name'))); } + $btn.disable().addClass('is-loading'); + return $.post(_this.import_url, { repo_id: id, - new_namespace: new_namespace + target_namespace: target_namespace }, { dataType: 'script' }); @@ -70,7 +73,7 @@ if ($('.js-importer-status').length) { var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); var importPath = $('.js-importer-status').data('import-path'); - + new ImporterStatus(jobsImportPath, importPath); } }); diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js.es6 index d0305c6c6a1..53faaa38a0c 100644 --- a/app/assets/javascripts/issuable.js +++ b/app/assets/javascripts/issuable.js.es6 @@ -8,6 +8,7 @@ Issuable.initTemplates(); Issuable.initSearch(); Issuable.initChecks(); + Issuable.initResetFilters(); return Issuable.initLabelFilterRemove(); }, initTemplates: function() { @@ -37,9 +38,11 @@ return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { var $button; $button = $(this); + // Remove the label input box $('input[name="label_name[]"]').filter(function() { return this.value === $button.data('label'); }).remove(); + // Submit the form to get new data Issuable.filterResults($('.filter-form')); return $('.js-label-select').trigger('update.label'); }); @@ -55,6 +58,17 @@ return Turbolinks.visit(issuesUrl); }; })(this), + initResetFilters: function() { + $('.reset-filters').on('click', function(e) { + e.preventDefault(); + const target = e.target; + const $form = $(target).parents('.js-filter-form'); + const baseIssuesUrl = target.href; + + $form.attr('action', baseIssuesUrl); + Turbolinks.visit(baseIssuesUrl); + }); + }, initChecks: function() { this.issuableBulkActions = $('.bulk-update').data('bulkActions'); $('.check_all_issues').off('click').on('click', function() { @@ -64,19 +78,22 @@ return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); }, checkChanged: function() { - var checked_issues, ids; - checked_issues = $('.selected_issue:checked'); - if (checked_issues.length > 0) { - ids = $.map(checked_issues, function(value) { + const $checkedIssues = $('.selected_issue:checked'); + const $updateIssuesIds = $('#update_issuable_ids'); + const $issuesOtherFilters = $('.issues-other-filters'); + const $issuesBulkUpdate = $('.issues_bulk_update'); + + if ($checkedIssues.length > 0) { + let ids = $.map($checkedIssues, function(value) { return $(value).data('id'); }); - $('#update_issues_ids').val(ids); - $('.issues-other-filters').hide(); - $('.issues_bulk_update').show(); + $updateIssuesIds.val(ids); + $issuesOtherFilters.hide(); + $issuesBulkUpdate.show(); } else { - $('#update_issues_ids').val([]); - $('.issues_bulk_update').hide(); - $('.issues-other-filters').show(); + $updateIssuesIds.val([]); + $issuesBulkUpdate.hide(); + $issuesOtherFilters.show(); this.issuableBulkActions.willUpdateLabels = false; } return true; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 6838d9d8da1..261bf6137c2 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,10 +1,6 @@ /*= require flash */ - - /*= require jquery.waitforimages */ - - /*= require task_list */ (function() { @@ -13,6 +9,7 @@ this.Issue = (function() { function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); + // Prevent duplicate event bindings this.disableTaskList(); if ($('a.btn-close').length) { this.initTaskList(); @@ -99,6 +96,8 @@ url: $('form.js-issuable-update').attr('action'), data: patchData }); + // TODO (rspeicher): Make the issue description inline-editable like a note so + // that we can re-use its form here }; Issue.prototype.initMergeRequests = function() { @@ -127,7 +126,9 @@ Issue.prototype.initCanCreateBranch = function() { var $container; - $container = $('div#new-branch'); + $container = $('#new-branch'); + // If the user doesn't have the required permissions the container isn't + // rendered at all. if ($container.length === 0) { return; } @@ -139,7 +140,6 @@ if (data.can_create_branch) { $container.find('.checking').hide(); $container.find('.available').show(); - return $container.find('a').attr('disabled', false); } else { $container.find('.checking').hide(); return $container.find('.unavailable').show(); diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js index 98d3358ba92..62a7fc9a06c 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js +++ b/app/assets/javascripts/issues-bulk-assignment.js @@ -1,14 +1,17 @@ (function() { this.IssuableBulkActions = (function() { function IssuableBulkActions(opts) { + // Set defaults var ref, ref1, ref2; if (opts == null) { opts = {}; } - this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue'); + this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li'); + // Save instance this.form.data('bulkActions', this); this.willUpdateLabels = false; this.bindEvents(); + // Fixes bulk-assign not working when navigating through pages Issuable.initChecks(); } @@ -86,6 +89,7 @@ ref1 = this.getLabelsFromSelection(); for (j = 0, len1 = ref1.length; j < len1; j++) { id = ref1[j]; + // Only the ones that we are not going to keep if (labelsToKeep.indexOf(id) === -1) { result.push(id); } @@ -106,7 +110,7 @@ state_event: this.form.find('input[name="update[state_event]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), - issues_ids: this.form.find('input[name="update[issues_ids]"]').val(), + issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), add_label_ids: [], remove_label_ids: [] @@ -147,6 +151,8 @@ indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); labelsToApply = this.getLabelsToApply(); indeterminatedLabels.map(function(id) { + // We need to exclude label IDs that will be applied + // By not doing this will cause issues from selection to not add labels at all if (labelsToApply.indexOf(id) === -1) { return result.push(id); } diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index fe071fca67c..cb16e2ba814 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -26,13 +26,16 @@ var previewColor; previewColor = $('input#label_color').val(); return $('div.label-color-preview').css('background-color', previewColor); + // Updates the the preview color with the hex-color input }; + // Updates the preview color with a click on a suggested color Labels.prototype.setSuggestedColor = function(e) { var color; color = $(e.currentTarget).data('color'); $('input#label_color').val(color); this.updateColorPreview(); + // Notify the form, that color has changed $('.label-form').trigger('keyup'); return e.preventDefault(); }; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 565dbeacdb3..29a967a35a0 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -156,15 +156,17 @@ selectedClass.push('is-indeterminate'); } if (active.indexOf(label.id) !== -1) { + // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); + // Add input manually instance.addInput(this.fieldName, label.id); } } - if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) { + if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + escape(this.id(label)) + "']").length) { selectedClass.push('is-active'); } if ($dropdown.hasClass('js-multiselect') && removesAll) { @@ -172,6 +174,7 @@ } if (label.duplicate) { spacing = 100 / label.color.length; + // Reduce the colors to 4 label.color = label.color.filter(function(color, i) { return i < 4; }); @@ -192,11 +195,13 @@ } else { colorEl = ''; } + // We need to identify which items are actually labels if (label.id) { selectedClass.push('label-item'); $a.attr('data-label-id', label.id); } $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + // Return generated html return $li.html($a).prop('outerHTML'); }, persistWhenHide: $dropdown.data('persistWhenHide'), @@ -238,6 +243,7 @@ isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; $selectbox.hide(); + // display:block overrides the hide-collapse rule $value.removeAttr('style'); if (page === 'projects:boards:show') { return; @@ -255,6 +261,7 @@ } } if ($dropdown.hasClass('js-filter-bulk-update')) { + // If we are persisting state we need the classes if (!this.options.persistWhenHide) { return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); } @@ -324,7 +331,9 @@ if ($('.selected_issue:checked').length) { return; } + // Remove inputs $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); + // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index 8d5e52286b7..d9b07c10a49 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index 8ee81804513..a88e640f298 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index 31e6033e756..ee1baf54803 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index 923c575dcfe..6df427bc2b1 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -1,13 +1,8 @@ /*= require raphael */ - - /*= require g.raphael */ - - /*= require g.bar */ (function() { - }).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 10afa7e4329..8fdf4646cd8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -29,6 +29,7 @@ if (setTimeago) { $timeagoEls.timeago(); $timeagoEls.tooltip('destroy'); + // Recreate with custom template return $timeagoEls.tooltip({ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' }); @@ -67,6 +68,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/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb deleted file mode 100644 index 80f9936b9c2..00000000000 --- a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb +++ /dev/null @@ -1,2 +0,0 @@ -gl.emojiAliases = -> - JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb new file mode 100644 index 00000000000..aeb86c9fa5b --- /dev/null +++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb @@ -0,0 +1,6 @@ +(function() { + gl.emojiAliases = function() { + return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>'); + }; + +}).call(this); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 42b6ac0589e..5b338b00d76 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -6,6 +6,7 @@ notification = new Notification(message, opts); setTimeout(function() { return notification.close(); + // Hide the notification after X amount of seconds }, 8000); if (onclick) { return notification.onclick = onclick; @@ -22,12 +23,16 @@ body: body, icon: icon }; + // Let's check if the browser supports notifications if (!('Notification' in window)) { + // do nothing } else if (Notification.permission === 'granted') { + // If it's okay let's create a notification return notificationGranted(message, opts, onclick); } else if (Notification.permission !== 'denied') { return Notification.requestPermission(function(permission) { + // If the user accepts, let's create a notification if (permission === 'granted') { return notificationGranted(message, opts, onclick); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index b6636de5767..d761a844be9 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -29,6 +29,7 @@ lineBefore = this.lineBefore(text, textArea); lineAfter = this.lineAfter(text, textArea); if (lineBefore === blockTag && lineAfter === blockTag) { + // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); @@ -63,11 +64,11 @@ if (!inserted) { try { document.execCommand("ms-beginUndoUnit"); - } catch (undefined) {} + } catch (error) {} textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); try { document.execCommand("ms-endUndoUnit"); - } catch (undefined) {} + } catch (error) {} } return this.moveCursor(textArea, tag, wrap); }; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index fffbfd19745..f84a20cf0fe 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -7,6 +7,8 @@ if ((base = w.gl).utils == null) { base.utils = {}; } + // Returns an array containing the value(s) of the + // of the key passed as an argument w.gl.utils.getParameterValues = function(sParam) { var i, sPageURL, sParameterName, sURLVariables, values; sPageURL = decodeURIComponent(window.location.search.substring(1)); @@ -23,6 +25,8 @@ } return values; }; + // @param {Object} params - url keys and value to merge + // @param {String} url w.gl.utils.mergeUrlParams = function(params, url) { var lastChar, newUrl, paramName, paramValue, pattern; newUrl = decodeURIComponent(url); @@ -37,13 +41,15 @@ newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; } } + // Remove a trailing ampersand lastChar = newUrl[newUrl.length - 1]; if (lastChar === '&') { newUrl = newUrl.slice(0, -1); } return newUrl; }; - return w.gl.utils.removeParamQueryString = function(url, param) { + // removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = function(url, param) { var urlVariables, variables; url = decodeURIComponent(url); urlVariables = url.split('&'); @@ -59,6 +65,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/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index f145bd3ad74..93daea1dce7 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,17 +1,49 @@ - +// LineHighlighter +// +// Handles single- and multi-line selection and highlight for blob views. +// /*= require jquery.scrollTo */ +// +// ### Example Markup +// +// <div id="blob-content-holder"> +// <div class="file-content"> +// <div class="line-numbers"> +// <a href="#L1" id="L1" data-line-number="1">1</a> +// <a href="#L2" id="L2" data-line-number="2">2</a> +// <a href="#L3" id="L3" data-line-number="3">3</a> +// <a href="#L4" id="L4" data-line-number="4">4</a> +// <a href="#L5" id="L5" data-line-number="5">5</a> +// </div> +// <pre class="code highlight"> +// <code> +// <span id="LC1" class="line">...</span> +// <span id="LC2" class="line">...</span> +// <span id="LC3" class="line">...</span> +// <span id="LC4" class="line">...</span> +// <span id="LC5" class="line">...</span> +// </code> +// </pre> +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; this.LineHighlighter = (function() { + // CSS class applied to highlighted lines LineHighlighter.prototype.highlightClass = 'hll'; + // Internal copy of location.hash so we're not dependent on `location` in tests LineHighlighter.prototype._hash = ''; function LineHighlighter(hash) { var range; if (hash == null) { + // Initialize a LineHighlighter object + // + // hash - String URL hash for dependency injection in tests hash = location.hash; } this.setHash = bind(this.setHash, this); @@ -24,6 +56,8 @@ if (range[0]) { this.highlightRange(range); $.scrollTo("#L" + range[0], { + // Scroll to the first highlighted line on initial load + // Offset -50 for the sticky top bar, and another -100 for some context offset: -150 }); } @@ -32,6 +66,12 @@ LineHighlighter.prototype.bindEvents = function() { $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); + // While it may seem odd to bind to the mousedown event and then throw away + // the click event, there is a method to our madness. + // + // If not done this way, the line number anchor will sometimes keep its + // active state even when the event is cancelled, resulting in an ugly border + // around the link and/or a persisted underline text decoration. return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { return event.preventDefault(); }); @@ -44,6 +84,8 @@ lineNumber = $(event.target).closest('a').data('line-number'); current = this.hashToRange(this._hash); if (!(current[0] && event.shiftKey)) { + // If there's no current selection, or there is but Shift wasn't held, + // treat this like a single-line selection. this.setHash(lineNumber); return this.highlightLine(lineNumber); } else if (event.shiftKey) { @@ -59,10 +101,23 @@ LineHighlighter.prototype.clearHighlight = function() { return $("." + this.highlightClass).removeClass(this.highlightClass); + // Unhighlight previously highlighted lines }; + // Convert a URL hash String into line numbers + // + // hash - Hash String + // + // Examples: + // + // hashToRange('#L5') # => [5, null] + // hashToRange('#L5-15') # => [5, 15] + // hashToRange('#foo') # => [null, null] + // + // Returns an Array LineHighlighter.prototype.hashToRange = function(hash) { var first, last, matches; + //?L(\d+)(?:-(\d+))?$/) matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); if (matches && matches.length) { first = parseInt(matches[1]); @@ -73,10 +128,16 @@ } }; + // Highlight a single line + // + // lineNumber - Line number to highlight LineHighlighter.prototype.highlightLine = function(lineNumber) { return $("#LC" + lineNumber).addClass(this.highlightClass); }; + // Highlight all lines within a range + // + // range - Array containing the starting and ending line numbers LineHighlighter.prototype.highlightRange = function(range) { var i, lineNumber, ref, ref1, results; if (range[1]) { @@ -90,6 +151,7 @@ } }; + // Set the URL hash string LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { var hash; if (lastLineNumber) { @@ -101,10 +163,15 @@ return this.__setLocationHash__(hash); }; + // Make the actual hash change in the browser + // + // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { return history.pushState({ turbolinks: false, url: value + // We're using pushState instead of assigning location.hash directly to + // prevent the page from scrolling on the hashchange event }, document.title, value); }; diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 218f24fe908..7d8eef1b495 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,54 +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); - }; - - 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++; - } - }; - - $(document).on('page:fetch', start); + $(document).on('page:fetch', function() { + $('.tanuki-logo').addClass('animate'); + }); - $(document).on('page:change', stop); + $(document).on('page:change', function() { + $('.tanuki-logo').removeClass('animate'); + }); }).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/merge_request.js b/app/assets/javascripts/merge_request.js index 56ebf84c4f6..05644b3d03c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,10 +1,6 @@ /*= require jquery.waitforimages */ - - /*= require task_list */ - - /*= require merge_request_tabs */ (function() { @@ -12,6 +8,11 @@ this.MergeRequest = (function() { function MergeRequest(opts) { + // Initialize MergeRequest behavior + // + // Options: + // action - String, current controller action + // this.opts = opts != null ? opts : {}; this.submitNoteForm = bind(this.submitNoteForm, this); this.$el = $('.merge-request'); @@ -21,6 +22,7 @@ }; })(this)); this.initTabs(); + // Prevent duplicate event bindings this.disableTaskList(); this.initMRBtnListeners(); if ($("a.btn-close").length) { @@ -28,14 +30,17 @@ } } + // Local jQuery finder MergeRequest.prototype.$ = function(selector) { return this.$el.find(selector); }; MergeRequest.prototype.initTabs = function() { if (this.opts.action !== 'new') { + // `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs = new MergeRequestTabs(this.opts); } else { + // Show the first tab (Commits) return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show'); } }; @@ -96,6 +101,8 @@ url: $('form.js-issuable-update').attr('action'), data: patchData }); + // TODO (rspeicher): Make the merge request description inline-editable like a + // note so that we can re-use its form here }; return MergeRequest; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index ad08209d61e..dcba4a8d275 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,6 +1,49 @@ - +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// /*= require jquery.cookie */ +// +// ### Example Markup +// +// <ul class="nav-links merge-request-tabs"> +// <li class="notes-tab active"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// Discussion +// </a> +// </li> +// <li class="commits-tab"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// Commits +// </a> +// </li> +// <li class="diffs-tab"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// Diffs +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="notes tab-pane active" id="notes"> +// Notes Content +// </div> +// <div class="commits tab-pane" id="commits"> +// Commits Content +// </div> +// <div class="diffs tab-pane" id="diffs"> +// Diffs Content +// </div> +// </div> +// +// <div class="mr-loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -19,6 +62,7 @@ this.setCurrentAction = bind(this.setCurrentAction, this); this.tabShown = bind(this.tabShown, this); this.showTab = bind(this.showTab, this); + // Store the `location` object, allowing for easier stubbing in tests this._location = location; this.bindEvents(); this.activateTab(this.opts.action); @@ -77,6 +121,7 @@ } }; + // Activate a tab based on the current action MergeRequestTabs.prototype.activateTab = function(action) { if (action === 'show') { action = 'notes'; @@ -84,20 +129,48 @@ return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); }; + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String MergeRequestTabs.prototype.setCurrentAction = function(action) { var new_state; + // Normalize action, just to be safe if (action === 'show') { action = 'notes'; } this.currentAction = action; + // Remove a trailing '/commits' or '/diffs' new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); + // Append the new action if we're on a tab other than 'notes' if (action !== 'notes') { new_state += "/" + action; } + // Ensure parameters and hash come along for the ride new_state += this._location.search + this._location.hash; history.replaceState({ turbolinks: true, url: new_state + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 }, document.title, new_state); return new_state; }; @@ -206,6 +279,9 @@ }); }; + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide MergeRequestTabs.prototype.toggleLoading = function(status) { return $('.mr-loading-status .loading').toggle(status); }; @@ -232,6 +308,7 @@ MergeRequestTabs.prototype.diffViewType = function() { return $('.inline-parallel-buttons a.active').data('view-type'); + // Returns diff view type }; MergeRequestTabs.prototype.expandViewContainer = function() { @@ -245,6 +322,8 @@ if ($gutterIcon.is('.fa-angle-double-right')) { return $gutterIcon.closest('a').trigger('click', [true]); } + // Wait until listeners are set + // Only when sidebar is expanded }, 0); }; @@ -259,6 +338,9 @@ return $gutterIcon.closest('a').trigger('click', [true]); } }, 0); + // Expand the issuable sidebar unless the user explicitly collapsed it + // Wait until listeners are set + // Only when sidebar is collapsed }; return MergeRequestTabs; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index bd35b6f679d..7bbcdf59838 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -3,6 +3,12 @@ this.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { + // Initialize MergeRequestWidget behavior + // + // check_enable - Boolean, whether to check automerge status + // merge_check_url - String, URL to use to check automerge status + // ci_status_url - String, URL to use to check CI status + // this.opts = opts; $('#modal_merge_info').modal({ show: false @@ -118,6 +124,8 @@ if (data.coverage) { _this.showCICoverage(data.coverage); } + // The first check should only update the UI, a notification + // should only be displayed on status changes if (showNotification && !_this.firstCICheck) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index e8d51da7d58..bc1a99057d9 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -110,6 +110,7 @@ }, update: function(event, ui) { var data; + // Prevents sorting from container which element has been removed. if ($(this).find(ui.item).length > 0) { data = $(this).sortable("serialize"); return Milestone.sortIssues(data); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e897ebdf630..c8031174dd2 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -92,6 +92,7 @@ }, hidden: function() { $selectbox.hide(); + // display:block overrides the hide-collapse rule return $value.css('display', ''); }, clicked: function(selected, $el, e) { diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js index c0fec1f8607..91132af273a 100644 --- a/app/assets/javascripts/network/branch-graph.js +++ b/app/assets/javascripts/network/branch-graph.js @@ -90,6 +90,7 @@ results = []; while (k < this.mspace) { this.colors.push(Raphael.getColor(.8)); + // Skipping a few colors in the spectrum to get more contrast between colors Raphael.getColor(); Raphael.getColor(); results.push(k++); @@ -112,6 +113,7 @@ for (mm = j = 0, len = ref.length; j < len; mm = ++j) { day = ref[mm]; if (cuday !== day[0] || cumonth !== day[1]) { + // Dates r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ font: "12px Monaco, monospace", fill: "#BBB" @@ -119,6 +121,7 @@ cuday = day[0]; } if (cumonth !== day[1]) { + // Months r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ font: "12px Monaco, monospace", fill: "#EEE" @@ -207,6 +210,7 @@ } r = this.r; shortrefs = commit.refs; + // Truncate if longer than 15 chars if (shortrefs.length > 17) { shortrefs = shortrefs.substr(0, 15) + "…"; } @@ -217,6 +221,7 @@ title: commit.refs }); textbox = text.getBBox(); + // Create rectangle based on the size of the textbox rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ fill: "#000", "fill-opacity": .5, @@ -229,6 +234,7 @@ }); label = r.set(rect, text); label.transform(["t", -rect.getBBox().width - 15, 0]); + // Set text to front return text.toFront(); }; @@ -283,11 +289,13 @@ parentY = this.offsetY + this.unitTime * parentCommit.time; parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); + // Set line color if (parentCommit.space <= commit.space) { color = this.colors[commit.space]; } else { color = this.colors[parentCommit.space]; } + // Build line shape if (parent[1] === commit.space) { offset = [0, 5]; arrow = "l-2,5,4,0,-2,-5,0,5"; @@ -298,13 +306,17 @@ offset = [-3, 3]; arrow = "l-5,0,2,4,3,-4,-4,2"; } + // Start point route = ["M", x + offset[0], y + offset[1]]; + // Add arrow if not first parent if (i > 0) { route.push(arrow); } + // Circumvent if overlap if (commit.space !== parentCommit.space || commit.space !== parent[1]) { route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); } + // End point route.push("L", parentX1, parentY); results.push(r.path(route).attr({ stroke: color, @@ -325,6 +337,7 @@ "fill-opacity": .5, stroke: "none" }); + // Displayed in the center return this.element.scrollTop(y - this.graphHeight / 2); } }; diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 6a7422a7755..67c3e645364 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,9 @@ - +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// /*= require_tree . */ (function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index d0d5cad813a..c6854f703fb 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,22 +1,10 @@ /*= require autosave */ - - /*= require autosize */ - - /*= require dropzone */ - - /*= require dropzone_input */ - - /*= require gfm_auto_complete */ - - /*= require jquery.atwho */ - - /*= require task_list */ (function() { @@ -60,26 +48,43 @@ } Notes.prototype.addBinding = function() { + // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote); + // catch note ajax errors $(document).on("ajax:error", ".js-main-target-form", this.addNoteError); + // change note in UI after update $(document).on("ajax:success", "form.edit-note", this.updateNote); + // Edit note link $(document).on("click", ".js-note-edit", this.showEditForm); $(document).on("click", ".note-edit-cancel", this.cancelEdit); + // Reopen and close actions for Issue/MR combined with note form submit $(document).on("click", ".js-comment-button", this.updateCloseButton); $(document).on("keyup input", ".js-note-text", this.updateTargetButtons); + // resolve a discussion $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion); + // remove a note (in general) $(document).on("click", ".js-note-delete", this.removeNote); + // delete note attachment $(document).on("click", ".js-note-attachment-delete", this.removeAttachment); + // reset main target form after submit $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton); $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm); + // reset main target form when clicking discard $(document).on("click", ".js-note-discard", this.resetMainTargetForm); + // update the file name when an attachment is selected $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); + // reply to diff/discussion notes $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); + // add diff note $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); + // hide diff note form $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); + // fetch notes when tab becomes visible $(document).on("visibilitychange", this.visibilityChange); + // when issue status changes, we need to refresh data $(document).on("issuable:change", this.refresh); + // when a key is clicked on the notes return $(document).on("keydown", ".js-note-text", this.keydownNoteText); }; @@ -112,6 +117,7 @@ return; } $textarea = $(e.target); + // Edit previous note when UP arrow is hit switch (e.which) { case 38: if ($textarea.val() !== '') { @@ -123,6 +129,7 @@ return myLastNoteEditBtn.trigger('click', [true, myLastNote]); } break; + // Cancel creating diff note or editing any note when ESCAPE is hit case 27: discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { @@ -247,10 +254,13 @@ votesBlock = $('.js-awards-block').eq(0); gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); return gl.awardsHandler.scrollToAwards(); + // render note if it not present in loaded list + // or skip if rendered } else if (this.isNewNote(note)) { this.note_ids.push(note.id); $notesList = $('ul.main-notes-list'); $notesList.append(note.html).syntaxHighlight(); + // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.initTaskList(); this.refresh(); @@ -291,19 +301,26 @@ row = form.closest("tr"); note_html = $(note.html); note_html.syntaxHighlight(); + // is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); if ((note.original_discussion_id != null) && discussionContainer.length === 0) { discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); } if (discussionContainer.length === 0) { + // insert the note and the reply button after the temp row row.after(note.diff_discussion_html); + // remove the note (will be added again below) row.next().find(".note").remove(); + // Before that, the container didn't exist discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); + // Add note to 'Changes' page discussions discussionContainer.append(note_html); + // Init discussion on 'Discussion' page if it is merge request page if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) { $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight(); } } else { + // append new note to all matching discussions discussionContainer.append(note_html); } @@ -327,11 +344,18 @@ Notes.prototype.resetMainTargetForm = function(e) { var form; form = $(".js-main-target-form"); + // remove validation errors form.find(".js-errors").remove(); + // reset text and preview form.find(".js-md-write-button").click(); form.find(".js-note-text").val("").trigger("input"); form.find(".js-note-text").data("autosave").reset(); - return this.updateTargetButtons(e); + + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + form.find('.js-autosize')[0].dispatchEvent(event); + + this.updateTargetButtons(e); }; Notes.prototype.reenableTargetFormSubmitButton = function() { @@ -349,9 +373,13 @@ Notes.prototype.setupMainTargetNoteForm = function() { var form; + // find the form form = $(".js-new-note-form"); + // Set a global clone of the form for later cloning this.formClone = form.clone(); + // show the form this.setupNoteForm(form); + // fix classes form.removeClass("js-new-note-form"); form.addClass("js-main-target-form"); form.find("#note_line_code").remove(); @@ -416,6 +444,7 @@ } this.renderDiscussionNote(note); + // cleanup after successfully creating a diff/discussion note this.removeDiscussionNoteForm($form); }; @@ -428,10 +457,12 @@ Notes.prototype.updateNote = function(_xhr, note, _status) { var $html, $note_li; + // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); gl.utils.localTimeAgo($('.js-timeago', $html)); $html.syntaxHighlight(); $html.find('.js-task-list-container').taskList('enable'); + // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + note.id); $note_li.replaceWith($html); @@ -456,15 +487,20 @@ note.addClass("is-editting"); form = note.find(".note-edit-form"); form.addClass('current-note-edit-form'); + // Show the attachment delete link note.find(".js-note-attachment-delete").show(); done = function($noteText) { var noteTextVal; + // Neat little trick to put the cursor at the end noteTextVal = $noteText.val(); + // Store the original note text in a data attribute to retrieve if a user cancels edit. form.find('form.edit-note').data('original-note', noteTextVal); return $noteText.val('').val(noteTextVal); }; new GLForm(form); if ((scrollTo != null) && (myLastNote != null)) { + // scroll to the bottom + // so the open of the last element doesn't make a jump $('html, body').scrollTop($(document).height()); return $('html, body').animate({ scrollTop: myLastNote.offset().top - 150 @@ -500,6 +536,7 @@ form = note.find(".current-note-edit-form"); note.removeClass("is-editting"); form.removeClass("current-note-edit-form"); + // Replace markdown textarea text with original note text. return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); }; @@ -515,6 +552,9 @@ var noteId; noteId = $(e.currentTarget).closest(".note").attr("id"); $(".note[id='" + noteId + "']").each((function(_this) { + // A same note appears in the "Discussion" and in the "Changes" tab, we have + // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, + // where $("#noteId") would return only one. return function(i, el) { var note, notes; note = $(el); @@ -528,13 +568,17 @@ } } + // check if this is the last note for this line if (notes.find(".note").length === 1) { + // "Discussions" tab notes.closest(".timeline-entry").remove(); + // "Changes" tab / commit view notes.closest("tr").remove(); } return note.remove(); }; })(this)); + // Decrement the "Discussions" counter only once return this.updateNotesCount(-1); }; @@ -566,10 +610,12 @@ var form, replyLink; form = this.formClone.clone(); replyLink = $(e.target).closest(".js-discussion-reply-button"); + // insert the form after the button replyLink .closest('.discussion-reply-holder') .hide() .after(form); + // show the form return this.setupDiscussionNoteForm(replyLink, form); }; @@ -584,6 +630,7 @@ */ Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { + // setup note target form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId"))); form.attr("data-line-code", dataHolder.data("lineCode")); form.find("#note_type").val(dataHolder.data("noteType")); @@ -631,6 +678,7 @@ addForm = false; notesContentSelector = ".notes_content"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; + // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineType = $link.data("lineType"); notesContentSelector += "." + lineType; @@ -647,6 +695,7 @@ e.target = replyButton[0]; $.proxy(this.replyToDiscussionNote, replyButton[0], e).call(); } else { + // In parallel view, the form may not be present in one of the panes noteForm = notesContent.find(".js-discussion-note-form"); if (noteForm.length === 0) { addForm = true; @@ -654,6 +703,7 @@ } } } else { + // add a notes row and insert the form row.after(rowCssToAdd); nextRow = row.next(); notesContent = nextRow.find(notesContentSelector); @@ -662,6 +712,7 @@ if (addForm) { newForm = this.formClone.clone(); newForm.appendTo(notesContent); + // show the form return this.setupDiscussionNoteForm($link, newForm); } }; @@ -680,12 +731,15 @@ glForm = form.data('gl-form'); glForm.destroy(); form.find(".js-note-text").data("autosave").reset(); + // show the reply button (will only work for replies) form .prev('.discussion-reply-holder') .show(); if (row.is(".js-temp-notes-holder")) { + // remove temporary row for diff lines return row.remove(); } else { + // only remove the form return form.remove(); } }; @@ -707,6 +761,7 @@ Notes.prototype.updateFormAttachment = function() { var filename, form; form = $(this).closest("form"); + // get only the basename filename = $(this).val().replace(/^.*[\\\/]/, ""); return form.find(".js-attachment-filename").text(filename); }; diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 5fd75799640..5200487814f 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -1,9 +1,15 @@ +// MarkdownPreview +// +// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, +// and showing a warning when more than `x` users are referenced. +// (function() { var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; this.MarkdownPreview = (function() { function MarkdownPreview() {} + // Minimum number of users referenced before triggering a warning MarkdownPreview.prototype.referenceThreshold = 10; MarkdownPreview.prototype.ajaxCache = {}; @@ -101,8 +107,10 @@ return; } lastTextareaPreviewed = $form.find('textarea.markdown-area'); + // toggle tabs $form.find(writeButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().addClass('active'); + // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); return markdownPreview.showPreview($form); @@ -113,8 +121,10 @@ return; } lastTextareaPreviewed = null; + // toggle tabs $form.find(writeButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().removeClass('active'); + // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); return $form.find('.md-preview-holder').hide(); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index a3eea316f67..30cd6f6e470 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -5,6 +5,7 @@ GitLabCrop = (function() { var FILENAMEREGEX; + // Matches everything but the file name FILENAMEREGEX = /^.*[\\\/]/; function GitLabCrop(input, opts) { @@ -17,11 +18,18 @@ this.onModalShow = bind(this.onModalShow, this); this.onPickImageClick = bind(this.onPickImageClick, this); this.fileInput = $(input); + // We should rename to avoid spec to fail + // Form will submit the proper input filed with a file using FormData this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger"); + // Set defaults this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg; + // Required params + // Ensure needed elements are jquery objects + // If selector is provided we will convert them to a jQuery Object this.filename = this.getElement(this.filename); this.previewImage = this.getElement(this.previewImage); this.pickImageEl = this.getElement(this.pickImageEl); + // Modal elements usually are outside the @form element this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop; this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn; this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; @@ -93,8 +101,8 @@ return this.modalCropImg.attr('src', '').cropper('destroy'); }; - GitLabCrop.prototype.onUploadImageBtnClick = function(e) { - e.preventDefault(); + GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image + e.preventDefault(); // Destroy cropper instance this.setBlob(); this.setPreview(); this.modalCrop.modal('hide'); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index ed1d87abafe..60f9fba5777 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -11,9 +11,11 @@ this.form = (ref = opts.form) != null ? ref : $('.edit-user'); $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() { return $(this).parents('form').submit(); + // Automatically submit the Preferences form when any of its radio buttons change }); $('#user_notification_email').on('change', function() { return $(this).parents('form').submit(); + // Automatically submit email form when it changes }); $('.update-username').on('ajax:before', function() { $('.loading-username').show(); @@ -76,6 +78,7 @@ }, complete: function() { window.scrollTo(0, 0); + // Enable submit button after requests ends return self.form.find(':input[disabled]').enable(); } }); @@ -93,6 +96,7 @@ if (comment && comment.length > 1 && $title.val() === '') { return $title.val(comment[1]).change(); } + // Extract the SSH Key title from its comment }); if (gl.utils.getPagePath() === 'profiles') { return new Profile(); diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index b95faadc8e7..d6e4d9f7ad8 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 4e1de4dfb72..a6c015299a0 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -11,25 +11,27 @@ url = $("#project_clone").val(); $('#project_clone').val(url); return $('.clone').text(url); + // Git protocol switcher + // Remove the active class for all buttons (ssh, http, kerberos if shown) + // Add the active class for the clicked button + // Update the input field + // Update the command line instructions }); + // Ref switcher this.initRefSwitcher(); $('.project-refs-select').on('change', function() { 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(); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 4925f0519f0..5bf900f3e1d 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -13,8 +13,11 @@ this.selectRowUp = bind(this.selectRowUp, this); this.filePaths = {}; this.inputElement = this.element.find(".file-finder-input"); + // init event this.initEvent(); + // focus text input box this.inputElement.focus(); + // load file list this.load(this.options.url); } @@ -42,6 +45,7 @@ } } }); + // init event }; ProjectFindFile.prototype.findFile = function() { @@ -49,8 +53,10 @@ searchText = this.inputElement.val(); result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; return this.renderList(result, searchText); + // find file }; + // files pathes load ProjectFindFile.prototype.load = function(url) { return $.ajax({ url: url, @@ -67,6 +73,7 @@ }); }; + // render result ProjectFindFile.prototype.renderList = function(filePaths, searchText) { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); @@ -86,6 +93,7 @@ return results; }; + // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) highlighter = function(element, text, matches) { var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; lastIndex = 0; @@ -110,6 +118,7 @@ return element.append(document.createTextNode(text.substring(lastIndex))); }; + // make tbody row html ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { var $tr; $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>"); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 798f15e40a0..a787b11f2a9 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -4,6 +4,8 @@ this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = bind(this.toggleSettings, this); + this.$selects = $('.features select'); + $('.project-edit-container').on('ajax:before', (function(_this) { return function() { $('.project-edit-container').hide(); @@ -15,18 +17,24 @@ } ProjectNew.prototype.toggleSettings = function() { - this._showOrHide('#project_builds_enabled', '.builds-feature'); - return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature'); + var self = this; + + this.$selects.each(function () { + var $select = $(this), + className = $select.data('field').replace(/_/g, '-') + .replace('access-level', 'feature'); + self._showOrHide($select, '.' + className); + }); }; ProjectNew.prototype.toggleSettingsOnclick = function() { - return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings); + this.$selects.on('change', this.toggleSettings); }; ProjectNew.prototype._showOrHide = function(checkElement, container) { - var $container; - $container = $(container); - if ($(checkElement).prop('checked')) { + var $container = $(container); + + if ($(checkElement).val() !== '0') { return $container.show(); } else { return $container.hide(); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index 8ca4c427912..c8cfc9a9ba8 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -7,3 +7,5 @@ })(); }).call(this); + +// I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 4f415b05dbc..04fb49552e8 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -33,6 +33,7 @@ $('.projects-list-holder').replaceWith(data.html); return history.replaceState({ page: project_filter_url + // Change url so if user reload a page - search results are saved }, document.title, project_filter_url); }, dataType: "json" diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 index 6738dc8862d..983322cbecc 100644 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -45,6 +45,7 @@ class ProtectedBranchDropdown { } onClickCreateWildcard() { + // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').selectRowAtIndex(0); } 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 227e8c696b4..8abb09c626f 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -24,6 +24,7 @@ this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this); this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this); this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || ''; + // Dropdown Element this.dropdown = this.wrap.find('.dropdown'); this.dropdownContent = this.dropdown.find('.dropdown-content'); this.locationBadgeEl = this.getElement('.location-badge'); @@ -35,6 +36,7 @@ this.repositoryInputEl = this.getElement('#repository_ref'); this.clearInput = this.getElement('.js-clear-input'); this.saveOriginalState(); + // Only when user is logged in if (gon.current_user_id) { this.createAutocomplete(); } @@ -43,6 +45,7 @@ this.bindEvents(); } + // Finds an element inside wrapper element SearchAutocomplete.prototype.getElement = function(selector) { return this.wrap.find(selector); }; @@ -82,6 +85,7 @@ } return; } + // Prevent multiple ajax calls if (this.loadingSuggestions) { return; } @@ -92,14 +96,17 @@ term: term }, function(response) { var data, firstCategory, i, lastCategory, len, suggestion; + // Hide dropdown menu if no suggestions returns if (!response.length) { _this.disableAutocomplete(); return; } data = []; + // List results firstCategory = true; for (i = 0, len = response.length; i < len; i++) { suggestion = response[i]; + // Add group header before list each group if (lastCategory !== suggestion.category) { if (!firstCategory) { data.push('separator'); @@ -119,6 +126,7 @@ url: suggestion.url }); } + // Add option to proceed with the search if (data.length) { data.push('separator'); data.push({ @@ -169,11 +177,13 @@ SearchAutocomplete.prototype.serializeState = function() { return { + // Search Criteria search_project_id: this.projectInputEl.val(), group_id: this.groupInputEl.val(), search_code: this.searchCodeInputEl.val(), repository_ref: this.repositoryInputEl.val(), scope: this.scopeInputEl.val(), + // Location badge _location: this.locationBadgeEl.text() }; }; @@ -194,6 +204,7 @@ SearchAutocomplete.prototype.enableAutocomplete = function() { var _this; + // No need to enable anything if user is not logged in if (!gon.current_user_id) { return; } @@ -206,18 +217,22 @@ }; SearchAutocomplete.prototype.onSearchInputKeyDown = function() { + // Saves last length of the entered text return this.saveTextLength(); }; SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) { switch (e.keyCode) { case KEYCODE.BACKSPACE: + // when trying to remove the location badge if (this.lastTextLength === 0 && this.badgePresent()) { this.removeLocationBadge(); } + // When removing the last character and no badge is present if (this.lastTextLength === 1) { this.disableAutocomplete(); } + // When removing any character from existin value if (this.lastTextLength > 1) { this.enableAutocomplete(); } @@ -232,9 +247,12 @@ case KEYCODE.DOWN: return; default: + // Handle the case when deleting the input value other than backspace + // e.g. Pressing ctrl + backspace or ctrl + x if (this.searchInput.val() === '') { this.disableAutocomplete(); } else { + // We should display the menu only when input is not empty if (e.keyCode !== KEYCODE.ENTER) { this.enableAutocomplete(); } @@ -243,7 +261,9 @@ this.wrap.toggleClass('has-value', !!e.target.value); }; + // Avoid falsy value to be returned SearchAutocomplete.prototype.onSearchInputClick = function(e) { + // Prevents closing the dropdown menu return e.stopImmediatePropagation(); }; @@ -267,6 +287,7 @@ SearchAutocomplete.prototype.onSearchInputBlur = function(e) { this.isFocused = false; this.wrap.removeClass('search-active'); + // If input is blank then restore state if (this.searchInput.val() === '') { return this.restoreOriginalState(); } @@ -311,6 +332,7 @@ results = []; for (i = 0, len = inputs.length; i < len; i++) { input = inputs[i]; + // _location isnt a input if (input === '_location') { break; } diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 3b28332854a..3aa8536d40a 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -86,6 +86,7 @@ var defaultStopCallback; defaultStopCallback = Mousetrap.stopCallback; return function(e, element, combo) { + // allowed shortcuts if textarea, input, contenteditable are focused if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { return false; } else { diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 6c78914d338..92ce31969e3 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -14,8 +14,10 @@ ShortcutsFindFile.__super__.constructor.call(this); _oldStopCallback = Mousetrap.stopCallback; Mousetrap.stopCallback = (function(_this) { + // override to fire shortcuts action when focus in textbox return function(event, element, combo) { if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { + // when press up/down key in textbox, cusor prevent to move to home/end event.preventDefault(); return false; } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 3f3a8a9dfd9..235bf4f95ec 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,7 +1,5 @@ /*= require mousetrap */ - - /*= require shortcuts_navigation */ (function() { @@ -43,16 +41,20 @@ if (selected.trim() === "") { return; } + // Put a '>' character before each non-empty line in the selection quote = _.map(selected.split("\n"), function(val) { if (val.trim() !== '') { return "> " + val + "\n"; } }); + // If replyField already has some content, add a newline before our quote separator = replyField.val().trim() !== "" && "\n" || ''; replyField.val(function(_, current) { return current + separator + quote.join('') + "\n"; }); + // Trigger autosave for the added text replyField.trigger('input'); + // Focus the input field return replyField.focus(); } }; diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js deleted file mode 100644 index bd0c1194b36..00000000000 --- a/app/assets/javascripts/sidebar.js +++ /dev/null @@ -1,41 +0,0 @@ -(function() { - var collapsed, expanded, toggleSidebar; - - collapsed = 'page-sidebar-collapsed'; - - expanded = 'page-sidebar-expanded'; - - toggleSidebar = function() { - $('.page-with-sidebar').toggleClass(collapsed + " " + expanded); - $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded"); - if ($.cookie('pin_nav') === 'true') { - $('.navbar-fixed-top').toggleClass('header-pinned-nav'); - $('.page-with-sidebar').toggleClass('page-sidebar-pinned'); - } - return setTimeout((function() { - var niceScrollBars; - niceScrollBars = $('.nav-sidebar').niceScroll(); - return niceScrollBars.updateScrollBar(); - }), 300); - }; - - $(document).off('click', 'body').on('click', 'body', function(e) { - var $nav, $target, $toggle, pageExpanded; - if ($.cookie('pin_nav') !== 'true') { - $target = $(e.target); - $nav = $target.closest('.sidebar-wrapper'); - pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded'); - $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle'); - if ($nav.length === 0 && pageExpanded && $toggle.length === 0) { - $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded'); - return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded'); - } - } - }); - - $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) { - e.preventDefault(); - return toggleSidebar(); - }); - -}).call(this); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 new file mode 100644 index 00000000000..755fac8107b --- /dev/null +++ b/app/assets/javascripts/sidebar.js.es6 @@ -0,0 +1,93 @@ +((global) => { + let singleton; + + const pinnedStateCookie = 'pin_nav'; + const sidebarBreakpoint = 1024; + + const pageSelector = '.page-with-sidebar'; + const navbarSelector = '.navbar-fixed-top'; + const sidebarWrapperSelector = '.sidebar-wrapper'; + const sidebarContentSelector = '.nav-sidebar'; + + const pinnedToggleSelector = '.js-nav-pin'; + const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle'; + + const pinnedPageClass = 'page-sidebar-pinned'; + const expandedPageClass = 'page-sidebar-expanded'; + + const pinnedNavbarClass = 'header-sidebar-pinned'; + const expandedNavbarClass = 'header-sidebar-expanded'; + + class Sidebar { + constructor() { + if (!singleton) { + singleton = this; + singleton.init(); + } + return singleton; + } + + init() { + this.isPinned = $.cookie(pinnedStateCookie) === 'true'; + this.isExpanded = ( + window.innerWidth >= sidebarBreakpoint && + $(pageSelector).hasClass(expandedPageClass) + ); + $(document) + .on('click', sidebarToggleSelector, () => this.toggleSidebar()) + .on('click', pinnedToggleSelector, () => this.togglePinnedState()) + .on('click', 'html, body', (e) => this.handleClickEvent(e)) + .on('page:change', () => this.renderState()); + this.renderState(); + } + + handleClickEvent(e) { + if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) { + const $target = $(e.target); + const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0; + const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0; + if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) { + this.toggleSidebar(); + } + } + } + + toggleSidebar() { + this.isExpanded = !this.isExpanded; + this.renderState(); + } + + togglePinnedState() { + this.isPinned = !this.isPinned; + if (!this.isPinned) { + this.isExpanded = false; + } + $.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', { + path: gon.relative_url_root || '/', + expires: 3650 + }); + this.renderState(); + } + + renderState() { + $(pageSelector) + .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded) + .toggleClass(expandedPageClass, this.isExpanded); + $(navbarSelector) + .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded) + .toggleClass(expandedNavbarClass, this.isExpanded); + + const $pinnedToggle = $(pinnedToggleSelector); + const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation'; + const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide'; + $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState); + + if (this.isExpanded) { + setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200); + } + } + } + + global.Sidebar = Sidebar; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 new file mode 100644 index 00000000000..6f0996c0d2a --- /dev/null +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -0,0 +1,11 @@ +(global => { + global.gl = global.gl || {}; + + gl.SnippetsList = function() { + var $holder = $('.snippets-list-holder'); + + $holder.find('.pagination').on('ajax:success', (e, data) => { + $holder.replaceWith(data.html); + }); + } +})(window); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index dba62638c78..2ae7bf5fc15 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,9 +1,20 @@ +// Syntax Highlighter +// +// Applies a syntax highlighting color scheme CSS class to any element with the +// `js-syntax-highlight` class +// +// ### Example Markup +// +// <div class="js-syntax-highlight"></div> +// (function() { $.fn.syntaxHighlight = function() { var $children; if ($(this).hasClass('js-syntax-highlight')) { + // Given the element itself, apply highlighting return $(this).addClass(gon.user_color_scheme); } else { + // Given a parent element, recurse to any of its applicable children $children = $(this).find('.js-syntax-highlight'); if ($children.length) { return $children.syntaxHighlight(); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 6e677fa8cc6..93421649ac7 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -13,6 +13,7 @@ this.perPage = this.el.data('perPage'); this.clearListeners(); this.initBtnListeners(); + this.initFilters(); } Todos.prototype.clearListeners = function() { @@ -27,6 +28,31 @@ return $('.todo').on('click', this.goToTodoUrl); }; + Todos.prototype.initFilters = function() { + new UsersSelect(); + this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); + this.initFilterDropdown($('.js-type-search'), 'type'); + this.initFilterDropdown($('.js-action-search'), 'action_id'); + + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); + }; + + Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) { + $dropdown.glDropdown({ + selectable: true, + filterable: searchFields ? true : false, + fieldName: fieldName, + search: { fields: searchFields }, + data: $dropdown.data('data'), + clicked: function() { + return $dropdown.closest('form.filter-form').submit(); + } + }) + }; + Todos.prototype.doneClicked = function(e) { var $this; e.preventDefault(); @@ -66,7 +92,7 @@ success: (function(_this) { return function(data) { $this.remove(); - $('.js-todos-list').remove(); + $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); return _this.updateBadges(data); }; })(this) @@ -103,16 +129,21 @@ var currPage, currPages, newPages, pageParams, url; currPages = this.getTotalPages(); currPage = this.getCurrentPage(); + // Refresh if no remaining Todos if (!total) { location.reload(); return; } + // Do nothing if no pagination if (!currPages) { return; } newPages = Math.ceil(total / this.getTodosPerPage()); + // Includes query strings url = location.href; + // If new total of pages is different than we have now if (newPages !== currPages) { + // Redirect to previous page if there's one available if (currPages > 1 && currPage === currPages) { pageParams = { page: currPages - 1 @@ -129,6 +160,7 @@ if (!todoLink) { return; } + // Allow Meta-Click or Mouse3-click to open in a new tab if (e.metaKey || e.which === 2) { e.preventDefault(); return window.open(todoLink, '_blank'); diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 78e159a7ed9..9b7be17c4fe 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -2,6 +2,8 @@ this.TreeView = (function() { function TreeView() { this.initKeyNav(); + // Code browser tree slider + // Make the entire tree-item row clickable, but not if clicking another link (like a commit message) $(".tree-content-holder .tree-item").on('click', function(e) { var $clickedEl, path; $clickedEl = $(e.target); @@ -15,6 +17,7 @@ } } }); + // Show the "Loading commit data" for only the first element $('span.log_loading:first').removeClass('hide'); } diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 9ba847fb0c2..ce2930c7fc7 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,3 +1,7 @@ +// Authenticate U2F (universal 2nd factor) devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> authenticated -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -15,6 +19,17 @@ this.appId = u2fParams.app_id; this.challenge = u2fParams.challenge; this.signRequests = u2fParams.sign_requests.map(function(request) { + // The U2F Javascript API v1.1 requires a single challenge, with + // _no challenges per-request_. The U2F Javascript API v1.0 requires a + // challenge per-request, which is done by copying the single challenge + // into every request. + // + // In either case, we don't need the per-request challenges that the server + // has generated, so we can remove them. + // + // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. + // This can be removed once we upgrade. + // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 return _(request).omit('challenge'); }); } @@ -41,6 +56,7 @@ })(this), 10); }; + // Rendering # U2FAuthenticate.prototype.templates = { "notSupported": "#js-authenticate-u2f-not-supported", "setup": '#js-authenticate-u2f-setup', @@ -75,6 +91,8 @@ U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { this.renderTemplate('authenticated'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. return this.container.find("#js-device-response").val(deviceResponse); }; diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index c87e0840df3..926912fa988 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,3 +1,7 @@ +// Register U2F (universal 2nd factor) devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> registered -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -39,6 +43,7 @@ })(this), 10); }; + // Rendering # U2FRegister.prototype.templates = { "notSupported": "#js-register-u2f-not-supported", "setup": '#js-register-u2f-setup', @@ -73,6 +78,8 @@ U2FRegister.prototype.renderRegistered = function(deviceResponse) { this.renderTemplate('registered'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. return this.container.find("#js-device-response").val(deviceResponse); }; diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js deleted file mode 100644 index b46390ad8f4..00000000000 --- a/app/assets/javascripts/user.js +++ /dev/null @@ -1,31 +0,0 @@ -(function() { - this.User = (function() { - function User(opts) { - this.opts = opts; - $('.profile-groups-avatars').tooltip({ - "placement": "top" - }); - this.initTabs(); - $('.hide-project-limit-message').on('click', function(e) { - var path; - path = '/'; - $.cookie('hide_project_limit_message', 'false', { - path: path - }); - $(this).parents('.project-limit-message').remove(); - return e.preventDefault(); - }); - } - - User.prototype.initTabs = function() { - return new UserTabs({ - parentEl: '.user-profile', - action: this.opts.action - }); - }; - - return User; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 new file mode 100644 index 00000000000..6889d3a7491 --- /dev/null +++ b/app/assets/javascripts/user.js.es6 @@ -0,0 +1,34 @@ +(global => { + global.User = class { + constructor(opts) { + this.opts = opts; + this.placeProfileAvatarsToTop(); + this.initTabs(); + this.hideProjectLimitMessage(); + } + + placeProfileAvatarsToTop() { + $('.profile-groups-avatars').tooltip({ + placement: 'top' + }); + } + + initTabs() { + return new UserTabs({ + parentEl: '.user-profile', + action: this.opts.action + }); + } + + hideProjectLimitMessage() { + $('.hide-project-limit-message').on('click', e => { + e.preventDefault(); + const path = gon.relative_url_root || '/'; + $.cookie('hide_project_limit_message', 'false', { + path: path + }); + $(this).parents('.project-limit-message').remove(); + }); + } + } +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js index e5e75701fee..8a657780eb6 100644 --- a/app/assets/javascripts/user_tabs.js +++ b/app/assets/javascripts/user_tabs.js @@ -1,3 +1,61 @@ +// UserTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the Users#show page. +// +// ### Example Markup +// +// <ul class="nav-links"> +// <li class="activity-tab active"> +// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> +// Activity +// </a> +// </li> +// <li class="groups-tab"> +// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> +// Groups +// </a> +// </li> +// <li class="contributed-tab"> +// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> +// Contributed projects +// </a> +// </li> +// <li class="projects-tab"> +// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> +// Personal projects +// </a> +// </li> +// <li class="snippets-tab"> +// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="tab-pane" id="activity"> +// Activity Content +// </div> +// <div class="tab-pane" id="groups"> +// Groups Content +// </div> +// <div class="tab-pane" id="contributed"> +// Contributed projects content +// </div> +// <div class="tab-pane" id="projects"> +// Projects content +// </div> +// <div class="tab-pane" id="snippets"> +// Snippets content +// </div> +// </div> +// +// <div class="loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -6,18 +64,23 @@ this.tabShown = bind(this.tabShown, this); var i, item, len, ref, ref1, ref2, ref3; this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document); + // Make jQuery object if selector is provided if (typeof this.parentEl === 'string') { this.parentEl = $(this.parentEl); } + // Store the `location` object, allowing for easier stubbing in tests this._location = location; + // Set tab states this.loaded = {}; ref3 = this.parentEl.find('.nav-links a'); for (i = 0, len = ref3.length; i < len; i++) { item = ref3[i]; this.loaded[$(item).attr('data-action')] = false; } + // Actions this.actions = Object.keys(this.loaded); this.bindEvents(); + // Set active tab if (this.action === 'show') { this.action = this.defaultAction; } @@ -25,6 +88,7 @@ } UserTabs.prototype.bindEvents = function() { + // Toggle event listeners return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown); }; @@ -74,6 +138,7 @@ tabSelector = 'div#' + action; _this.parentEl.find(tabSelector).html(data.html); _this.loaded[action] = true; + // Fix tooltips return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); }; })(this) @@ -97,13 +162,17 @@ UserTabs.prototype.setCurrentAction = function(action) { var new_state, regExp; + // Remove possible actions from URL regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$'); new_state = this._location.pathname; + // remove trailing slashes new_state = new_state.replace(/\/+$/, ""); new_state = new_state.replace(regExp, ''); + // Append the new action if we're on a tab other than 'activity' if (action !== this.defaultAction) { new_state += "/" + action; } + // Ensure parameters and hash come along for the ride new_state += this._location.search + this._location.hash; history.replaceState({ turbolinks: true, diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 8b3dbf5f5ae..b8da7c4f297 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 = ''; @@ -12,29 +11,46 @@ this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.months = []; + // Loop through the timestamps to create a group of objects + // The group of objects will be grouped based on the day of the week they are 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]; + + // Create a new group array if this is the first day of the week + // or if is first object + if ((day === 0 && i !== 0) || i === 0) { + this.timestampsTmp.push([]); + group++; + } + + var innerArray = this.timestampsTmp[group - 1]; + // Push to the inner array the values that will be used to render map + innerArray.push({ + count: count || 0, + date: date, + day: day + }); + } + + // Init color functions this.colorKey = this.initColorKey(); this.color = this.initColor(); + // Init the svg element this.renderSvg(group); this.renderDays(); this.renderMonths(); @@ -43,8 +59,22 @@ this.initTooltips(); } + // Add extra padding for the last month label if it is also the last column + Calendar.prototype.getExtraWidthPadding = function(group) { + var extraWidthPadding = 0; + var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth(); + var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); + + if (lastColMonth != secondLastColMonth) { + extraWidthPadding = 3; + } + + return extraWidthPadding; + } + Calendar.prototype.renderSvg = function(group) { - return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar'); + var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); + return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar'); }; Calendar.prototype.renderDays = function() { diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index b95faadc8e7..d6e4d9f7ad8 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -3,5 +3,4 @@ (function() { - }).call(this); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index bad82868ab0..9c277998db4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -81,6 +81,7 @@ if (term.length === 0) { showDivider = 0; if (firstUser) { + // Move current user to the front of the list for (index = j = 0, len = users.length; j < len; index = ++j) { obj = users[index]; if (obj.username === firstUser) { @@ -115,6 +116,7 @@ if (showDivider) { users.splice(showDivider, 0, "divider"); } + // Send the data back return callback(users); }); }, @@ -139,6 +141,7 @@ inputId: 'issue_assignee_id', hidden: function(e) { $selectbox.hide(); + // display:block overrides the hide-collapse rule return $value.css('display', ''); }, clicked: function(user, $el, e) { @@ -177,6 +180,7 @@ img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />"; } } + // split into three parts so we can remove the username section if nessesary listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; listClosingTags = "</a> </li>"; @@ -215,6 +219,7 @@ }; if (query.term.length === 0) { if (firstUser) { + // Move current user to the front of the list ref = data.results; for (index = j = 0, len = ref.length; j < len; index = ++j) { obj = ref[index]; @@ -271,6 +276,7 @@ return _this.formatSelection.apply(_this, args); }, dropdownCssClass: "ajax-users-dropdown", + // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } @@ -318,6 +324,8 @@ }); }; + // Return users list. Filtered by query + // Only active users retrieved UsersSelect.prototype.users = function(query, options, callback) { var url; url = this.buildUrl(this.usersPath); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 71236c6a27d..777b32b41c9 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,21 +1,34 @@ - +// Zen Mode (full screen) textarea +// /*= provides zen_mode:enter */ - - /*= provides zen_mode:leave */ - - +// /*= require jquery.scrollTo */ - - /*= require dropzone */ - - /*= require mousetrap */ - - /*= require mousetrap/pause */ +// +// ### Events +// +// `zen_mode:enter` +// +// Fired when the "Edit in fullscreen" link is clicked. +// +// **Synchronicity** Sync +// **Bubbles** Yes +// **Cancelable** No +// **Target** a.js-zen-enter +// +// `zen_mode:leave` +// +// Fired when the "Leave Fullscreen" link is clicked. +// +// **Synchronicity** Sync +// **Bubbles** Yes +// **Cancelable** No +// **Target** a.js-zen-leave +// (function() { this.ZenMode = (function() { function ZenMode() { @@ -40,6 +53,7 @@ }; })(this)); $(document).on('keydown', function(e) { + // Esc if (e.keyCode === 27) { e.preventDefault(); return $(document).trigger('zen_mode:leave'); @@ -52,6 +66,7 @@ this.active_backdrop = $(backdrop); this.active_backdrop.addClass('fullscreen'); this.active_textarea = this.active_backdrop.find('textarea'); + // Prevent a user-resized textarea from persisting to fullscreen this.active_textarea.removeAttr('style'); return this.active_textarea.focus(); }; 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/animations.scss b/app/assets/stylesheets/framework/animations.scss index 1fec61bdba1..1e9a45c19b8 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -8,65 +8,44 @@ // Copyright (c) 2016 Daniel Eden .animated { - -webkit-animation-duration: 1s; - animation-duration: 1s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.animated.infinite { - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} + @include webkit-prefix(animation-duration, 1s); + @include webkit-prefix(animation-fill-mode, both); -.animated.hinge { - -webkit-animation-duration: 2s; - animation-duration: 2s; -} + &.infinite { + @include webkit-prefix(animation-iteration-count, infinite); + } -.animated.flipOutX, -.animated.flipOutY, -.animated.bounceIn, -.animated.bounceOut { - -webkit-animation-duration: .75s; - animation-duration: .75s; -} + &.once { + @include webkit-prefix(animation-iteration-count, 1); + } -@-webkit-keyframes pulse { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); + &.hinge { + @include webkit-prefix(animation-duration, 2s); } - 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); + &.flipOutX, + &.flipOutY, + &.bounceIn, + &.bounceOut { + @include webkit-prefix(animation-duration, .75s); } - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); + &.short { + @include webkit-prefix(animation-duration, 321ms); + @include webkit-prefix(animation-fill-mode, none); } } -@keyframes pulse { - from { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); +@include keyframes(pulse) { + from, to { + @include webkit-prefix(transform, scale3d(1, 1, 1)); } 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); - } - - to { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); + @include webkit-prefix(transform, scale3d(1.05, 1.05, 1.05)); } } .pulse { - -webkit-animation-name: pulse; - animation-name: pulse; + @include webkit-prefix(animation-name, pulse); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 7ce203d2ec7..f5223207f3a 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -249,6 +249,10 @@ > .controls { float: right; } + + .new-branch { + margin-top: 3px; + } } .content-block-small { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 6c3786b49bb..4618687a4be 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -200,13 +200,15 @@ svg { height: 15px; - width: auto; + width: 15px; position: relative; top: 2px; } svg, .fa { - margin-right: 3px; + &:not(:last-child) { + margin-right: 3px; + } } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c1e5305644b..5957dce89bc 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -53,7 +53,7 @@ pre { &.well-pre { border: 1px solid #eee; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #555; } @@ -225,7 +225,7 @@ li.note { .milestone { &.milestone-closed { - background: #f9f9f9; + background: $gray-light; } .progress { margin-bottom: 0; @@ -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 7d3a063d6c2..b0ba112476b 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 { @@ -177,6 +183,13 @@ &.dropdown-menu-user-link { line-height: 16px; } + + .icon-play { + fill: $table-text-gray; + margin-right: 6px; + height: 12px; + width: 11px; + } } .dropdown-header { @@ -189,6 +202,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 d3e3fc50736..76a3c083697 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -63,7 +63,7 @@ &.image_file { background: #eee; text-align: center; - + img { padding: 20px; max-width: 80%; @@ -94,7 +94,6 @@ &.blame { table { border: none; - box-shadow: none; margin: 0; } tr { @@ -108,19 +107,10 @@ border-right: none; } } - img.avatar { - border: 0 none; - float: none; - margin: 0; - padding: 0; - } td.blame-commit { - background: #f9f9f9; - min-width: 350px; - - .commit-author-link { - color: #888; - } + padding: 0 10px; + min-width: 400px; + background: $gray-light; } td.line-numbers { float: none; @@ -133,12 +123,6 @@ } td.lines { padding: 0; - code { - font-family: $monospace_font; - } - pre { - margin: 0; - } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 9209347f9bc..19827943385 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -1,6 +1,10 @@ .filter-item { margin-right: 6px; vertical-align: top; + + &.reset-filters { + padding: 7px; + } } @media (min-width: $screen-sm-min) { 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..d4a030f7f7a 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; } } @@ -87,14 +77,10 @@ header { } } - &.header-collapsed { - padding: 0 16px; - } - .side-nav-toggle { position: absolute; left: -10px; - margin: 6px 0; + margin: 7px 0; font-size: 18px; padding: 6px 10px; border: none; @@ -146,6 +132,8 @@ header { } .title { + position: relative; + padding-right: 20px; margin: 0; font-size: 19px; max-width: 400px; @@ -158,7 +146,11 @@ header { vertical-align: top; white-space: nowrap; - @media (max-width: $screen-sm-max) { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + + @media (max-width: $screen-xs-max) { max-width: 190px; } @@ -170,11 +162,15 @@ header { } .dropdown-toggle-caret { - position: relative; - top: -2px; + color: $gl-text-color; + border: transparent; + background: transparent; + position: absolute; + right: 3px; width: 12px; - line-height: 12px; - margin-left: 5px; + line-height: 19px; + margin-top: (($header-height - 19) / 2); + padding: 0; font-size: 10px; text-align: center; cursor: pointer; @@ -205,26 +201,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..1ec08cdef23 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -9,43 +9,11 @@ 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; - -ms-transition: $transition; - -o-transition: $transition; - transition: $transition; -} - /** * Prefilled mixins * 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; @@ -76,7 +44,7 @@ } &.active { - background: #f9f9f9; + background: $gray-light; a { font-weight: 600; } @@ -94,23 +62,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 +80,18 @@ color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.1); } + +@mixin webkit-prefix($property, $value) { + #{'-webkit-' + $property}: $value; + #{$property}: $value; +} + +@mixin keyframes($animation-name) { + @-webkit-keyframes #{$animation-name} { + @content; + } + + @keyframes #{$animation-name} { + @content; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 9e924f99e9c..553768b2e68 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; } } @@ -141,7 +138,7 @@ } li a { - padding: 16px 10px 11px; + padding: 16px 15px 11px; } /* Small devices (phones, tablets, 768px and lower) */ @@ -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 b2e22b60440..c75dacf95d9 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -151,7 +151,7 @@ background-position: right 0 bottom 6px; border: 1px solid $input-border; @include border-radius($border-radius-default); - @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { border-color: $input-border-focus; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 015fe3debf9..3b7de4b57bb 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,6 +1,5 @@ .page-with-sidebar { - padding-top: $header-height; - padding-bottom: 25px; + padding: $header-height 0 25px; transition: padding $sidebar-transition-duration; &.page-sidebar-pinned { @@ -15,6 +14,7 @@ bottom: 0; left: 0; height: 100%; + width: 0; overflow: hidden; transition: width $sidebar-transition-duration; @include box-shadow(2px 0 16px 0 $black-transparent); @@ -128,10 +128,8 @@ .fa { transition: transform .15s; - } - &.is-active { - .fa { + .page-sidebar-pinned & { transform: rotate(90deg); } } @@ -152,14 +150,6 @@ } } -.page-sidebar-collapsed { - padding-left: 0; - - .sidebar-wrapper { - width: 0; - } -} - .page-sidebar-expanded { .sidebar-wrapper { width: $sidebar_width; @@ -175,7 +165,7 @@ } } -header.header-pinned-nav { +header.header-sidebar-pinned { @media (min-width: $sidebar-breakpoint) { padding-left: ($sidebar_width + $gl-padding); diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 371c1bf17e1..915aa631ef8 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -125,7 +125,7 @@ $panel-inner-border: $border-color; // //## -$well-bg: #f9f9f9; +$well-bg: $gray-light; $well-border: #eee; //== Code diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 06874a993fa..3f8433a0e7f 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -159,25 +159,18 @@ position: relative; a.anchor { - // Setting `display: none` would prevent the anchor being scrolled to, so - // instead we set the height to 0 and it gets updated on hover. - height: 0; + left: -16px; + position: absolute; + text-decoration: none; + + &:after { + content: url('icon_anchor.svg'); + visibility: hidden; + } } - &:hover > a.anchor { - $size: 14px; - position: absolute; - right: 100%; - top: 50%; - margin-top: -11px; - margin-right: 0; - padding-right: 15px; - display: inline-block; - width: $size; - height: $size; - background-image: image-url("icon-link.png"); - background-size: contain; - background-repeat: no-repeat; + &:hover > a.anchor:after { + visibility: visible; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 5da390118c6..9f563a4de35 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,12 +10,78 @@ $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; /* + * Color schema + */ +$white-light: #fff; +$white-normal: #ededed; +$white-dark: #ececec; + +$gray-light: #fafafa; +$gray-normal: #f5f5f5; +$gray-dark: #ededed; +$gray-darkest: #c9c9c9; + +$green-light: #38ae67; +$green-normal: #2faa60; +$green-dark: #2ca05b; + +$blue-light: #2ea8e5; +$blue-normal: #2d9fd8; +$blue-dark: #2897ce; + +$blue-medium-light: #3498cb; +$blue-medium: #2f8ebf; +$blue-medium-dark: #2d86b4; + +$orange-light: #fc8a51; +$orange-normal: #e75e40; +$orange-dark: #ce5237; + +$red-light: #e52c5a; +$red-normal: #d22852; +$red-dark: darken($red-normal, 5%); + +$black: #000; +$black-transparent: rgba(0, 0, 0, 0.3); + +$border-white-light: #f1f2f4; +$border-white-normal: #d6dae2; +$border-white-dark: #c6cacf; + +$border-gray-light: #dcdcdc; +$border-gray-normal: #d7d7d7; +$border-gray-dark: #c6cacf; + +$border-green-light: #2faa60; +$border-green-normal: #2ca05b; +$border-green-dark: #279654; + +$border-blue-light: #2d9fd8; +$border-blue-normal: #2897ce; +$border-blue-dark: #258dc1; + +$border-orange-light: #fc6d26; +$border-orange-normal: #ce5237; +$border-orange-dark: #c14e35; + +$border-red-light: #d22852; +$border-red-normal: #ca264f; +$border-red-dark: darken($border-red-normal, 5%); + +$help-well-bg: $gray-light; +$help-well-border: #e5e5e5; + +$warning-message-bg: #fbf2d9; +$warning-message-color: #9e8e60; +$warning-message-border: #f0e2bb; + +/* * UI elements */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; $table-border-color: #f0f0f0; -$background-color: #fafafa; +$background-color: $gray-light; $dark-background-color: #f5f5f5; $table-text-gray: #8f8f8f; @@ -35,6 +101,7 @@ $gl-icon-color: $gl-placeholder-color; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; +$gl-gray-light: $gl-placeholder-color; $gl-header-color: $gl-title-color; /* @@ -90,73 +157,6 @@ $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; -/* - * Color schema - */ - -$white-light: #fff; -$white-normal: #ededed; -$white-dark: #ececec; - -$gray-light: #faf9f9; -$gray-normal: #f5f5f5; -$gray-dark: #ededed; -$gray-darkest: #c9c9c9; - -$green-light: #38ae67; -$green-normal: #2faa60; -$green-dark: #2ca05b; - -$blue-light: #2ea8e5; -$blue-normal: #2d9fd8; -$blue-dark: #2897ce; - -$blue-medium-light: #3498cb; -$blue-medium: #2f8ebf; -$blue-medium-dark: #2d86b4; - -$orange-light: #fc8a51; -$orange-normal: #e75e40; -$orange-dark: #ce5237; - -$red-light: #e52c5a; -$red-normal: #d22852; -$red-dark: darken($red-normal, 5%); - -$black: #000; -$black-transparent: rgba(0, 0, 0, 0.3); - -$border-white-light: #f1f2f4; -$border-white-normal: #d6dae2; -$border-white-dark: #c6cacf; - -$border-gray-light: #dcdcdc; -$border-gray-normal: #d7d7d7; -$border-gray-dark: #c6cacf; - -$border-green-light: #2faa60; -$border-green-normal: #2ca05b; -$border-green-dark: #279654; - -$border-blue-light: #2d9fd8; -$border-blue-normal: #2897ce; -$border-blue-dark: #258dc1; - -$border-orange-light: #fc6d26; -$border-orange-normal: #ce5237; -$border-orange-dark: #c14e35; - -$border-red-light: #d22852; -$border-red-normal: #ca264f; -$border-red-dark: darken($border-red-normal, 5%); - -$help-well-bg: #fafafa; -$help-well-border: #e5e5e5; - -$warning-message-bg: #fbf2d9; -$warning-message-color: #9e8e60; -$warning-message-border: #f0e2bb; - /* tanuki logo colors */ $tanuki-red: #e24329; $tanuki-orange: #fc6d26; @@ -186,7 +186,7 @@ $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; -$match-line: #fafafa; +$match-line: $gray-light; $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; @@ -267,7 +267,7 @@ $zen-control-hover-color: #111; $calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); -$calendar-unselectable-bg: #faf9f9; +$calendar-unselectable-bg: $gray-light; /* * Personal Access Tokens 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/awards.scss b/app/assets/stylesheets/pages/awards.scss index 5faedfedd66..9282e0ae03b 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -93,11 +93,8 @@ } .award-control { - margin-right: 5px; - margin-bottom: 5px; - padding-left: 5px; - padding-right: 5px; - line-height: 20px; + margin: 3px 5px 3px 0; + padding: 6px 5px; outline: 0; &:hover, diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9ac4d801ac4..037278bb083 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -10,7 +10,7 @@ .is-dragging { // Important because plugin sets inline CSS opacity: 1!important; - + * { // !important to make sure no style can override this when dragging cursor: -webkit-grabbing!important; @@ -142,11 +142,6 @@ } } -.board-header-loading-spinner { - margin-right: 10px; - color: $gray-darkest; -} - .board-inner-container { border-bottom: 1px solid $border-color; padding: $gl-padding; @@ -160,40 +155,6 @@ border-bottom: 1px solid $border-color; } -.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 { margin-right: 10px; padding: 0; @@ -304,3 +265,22 @@ margin-right: 8px; font-weight: 500; } + +.issue-boards-search { + width: 335px; + + .form-control { + display: inline-block; + width: 210px; + } +} + +.board-list-count { + padding: 10px 0; + color: $gl-placeholder-color; + font-size: 13px; + + > .fa { + margin-right: 5px; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index c1bb250b42d..c879074c7fe 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -36,6 +36,7 @@ &.affix { right: 30px; bottom: 15px; + z-index: 1; @media (min-width: $screen-md-min) { right: 26%; @@ -47,12 +48,6 @@ margin-bottom: 10px; } } - - .page-sidebar-collapsed { - .scroll-controls { - left: 70px; - } - } } .build-header { @@ -107,13 +102,23 @@ } .blocks-container { - padding: $gl-padding; + padding: 0 $gl-padding; } .block { width: 100%; } + .js-build-variable { + color: $code-color; + } + + .js-build-value { + padding: 2px 4px; + color: $black; + background-color: $white-light; + } + .build-sidebar-header { padding: 0 $gl-padding $gl-padding; @@ -122,6 +127,13 @@ } } + .retry-link { + color: $gl-link-color; + &:hover { + text-decoration: underline; + } + } + .stage-item { cursor: pointer; @@ -131,7 +143,7 @@ } .build-dropdown { - padding: 0 $gl-padding; + padding: $gl-padding 0; .dropdown-menu-toggle { margin-top: 8px; @@ -145,12 +157,11 @@ } .builds-container { - margin-top: $gl-padding; background-color: $white-light; border-top: 1px solid $border-color; border-bottom: 1px solid $border-color; max-height: 300px; - overflow: scroll; + overflow: auto; svg { position: relative; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 6a58b445afa..dc57a837155 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -18,8 +18,7 @@ } .commit-row-title { - line-height: 1; - margin-bottom: 7px; + line-height: 1.35; .notes_count { float: right; @@ -43,6 +42,7 @@ border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; + line-height: 1; &:hover { background-color: darken($gray-light, 10%); @@ -113,11 +113,13 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid #eee; + border-left: 1px solid $btn-gray-hover; padding: 10px 15px; margin: 10px 0; - background: #f9f9f9; + background: $gray-light; display: none; + white-space: pre-line; + word-break: normal; pre { border: none; @@ -134,7 +136,7 @@ .commit-row-info { color: $gl-gray; - line-height: 1; + line-height: 1.35; a { color: $gl-gray; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 55f9d4a0011..d01c60ee6ab 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -4,8 +4,9 @@ margin: 0; } - .fa-play { - font-size: 14px; + .icon-play { + height: 13px; + width: 12px; } .dropdown-new { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5c336bb1c7e..1d00da1266c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -60,7 +60,7 @@ pre { border: none; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #777; margin: 0 20px; @@ -92,7 +92,7 @@ border: 1px solid #eee; padding: 5px; @include border-radius(5px); - background: #f9f9f9; + background: $gray-light; margin-left: 10px; top: -6px; img { @@ -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/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 46c4a11aa2e..63845e6b9ba 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -404,3 +404,18 @@ margin-bottom: $gl-padding; } } + +.issuable-list { + li { + .issue-check { + float: left; + padding-right: $gl-padding; + margin-bottom: 10px; + min-width: 15px; + + .selected_issue { + vertical-align: text-top; + } + } + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index dfe1e3075da..7a26b7ad497 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -7,13 +7,6 @@ margin-bottom: 2px; } - .issue-check { - float: left; - padding-right: 8px; - margin-bottom: 10px; - min-width: 15px; - } - .issue-labels { display: inline-block; } @@ -68,12 +61,12 @@ form.edit-issue { } &.closed { - background: #f9f9f9; + background: $gray-light; border-color: #e5e5e5; } &.merged { - background: #f9f9f9; + background: $gray-light; border-color: #e5e5e5; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 606459f82cd..38c7cd98e41 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -7,6 +7,7 @@ display: inline-block; margin-right: 10px; margin-bottom: 10px; + text-decoration: none; } &.suggest-colors-dropdown { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 1f499897c16..5ec660799e3 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -16,7 +16,7 @@ $colors: ( white_button_origin_chosen : #268ced, white_header_not_chosen : #f0f0f0, - white_line_not_chosen : #f9f9f9, + white_line_not_chosen : $gray-light, dark_header_head_neutral : rgba(#3f3, .2), diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index fcdaf671538..2a44b95de64 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; } } @@ -375,6 +375,16 @@ } } +.mr-version-controls { + background: $background-color; + padding: $gl-btn-padding; + color: $gl-placeholder-color; + + a.btn-link { + color: $gl-dark-link-color; + } +} + .merge-request-details { .title { 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 6fa097e3bf1..2d66ab25da6 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -2,6 +2,7 @@ .stage { max-width: 90px; width: 90px; + text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -146,6 +147,7 @@ } .stage-cell { + text-align: center; svg { height: 18px; @@ -153,10 +155,6 @@ vertical-align: middle; overflow: visible; } - - .light { - width: 3px; - } } .duration, @@ -215,6 +213,13 @@ border-color: $border-white-normal; } } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } } } @@ -254,7 +259,6 @@ width: 100%; overflow: auto; white-space: nowrap; - max-height: 500px; transition: max-height 0.3s, padding 0.3s; &.graph-collapsed { @@ -265,7 +269,6 @@ .pipeline-visualization { position: relative; - min-width: 1220px; ul { padding: 0; @@ -275,7 +278,7 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 50px; + margin-right: 65px; li { list-style: none; @@ -321,6 +324,14 @@ a { color: $layout-link-gray; + text-decoration: none; + + &:hover { + .ci-status-text { + text-decoration: underline; + } + } + } } @@ -336,9 +347,9 @@ content: ''; position: absolute; top: 50%; - right: -54px; + right: -69px; border-top: 2px solid $border-color; - width: 54px; + width: 69px; height: 1px; } } @@ -358,22 +369,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; } } } @@ -392,6 +406,12 @@ border: none; } } + // Remove opposite curve + .curve { + &::before { + display: none; + } + } } } @@ -403,6 +423,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; } } } @@ -421,11 +474,22 @@ .pipelines.tab-pane { .content-list.pipelines { - overflow: scroll; + overflow: auto; } .stage { - max-width: 60px; - width: 60px; + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } +} + +.ci-status-icon-created { + + svg { + fill: $gray-darkest; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index eaf2d3270b3..3e6e50375f6 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; + } } } @@ -708,9 +723,15 @@ pre.light-well { } } -.project-refs-form { - .dropdown-menu { - width: 300px; +.project-refs-form .dropdown-menu, .dropdown-menu-projects { + width: 300px; + + @media (min-width: $screen-sm-min) { + width: 500px; + } + + a { + white-space: normal; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index c9d436d72ba..436fb00ba2e 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -80,7 +80,7 @@ .search-icon { @extend .fa-search; - @include transition(color .15s); + transition: color 0.15s; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -125,7 +125,7 @@ } .location-badge { - @include transition(all .15s); + transition: all 0.15s; background-color: $location-badge-active-bg; color: $white-light; } 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/todos.scss b/app/assets/stylesheets/pages/todos.scss index 0340526a53a..68a5d1ae06c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -99,7 +99,7 @@ pre { border: none; - background: #f9f9f9; + background: $gray-light; border-radius: 0; color: #777; margin: 0 20px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 9da40fe2b09..1778c069706 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -11,6 +11,10 @@ } } + .add-to-tree { + vertical-align: top; + } + .tree-table { margin-bottom: 0; @@ -22,6 +26,15 @@ line-height: 21px; } + .last-commit { + @include str-truncated(60%); + } + + .commit-history-link-spacer { + margin: 0 10px; + color: $table-border-color; + } + &:hover { td { background-color: $row-hover; @@ -77,11 +90,17 @@ } } - .tree_commit { - color: $gl-gray; + .tree-time-ago { + min-width: 135px; + color: $gl-gray-light; + } + + .tree-commit { + max-width: 320px; + color: $gl-gray-light; .tree-commit-link { - color: $gl-gray; + color: $gl-gray-light; &:hover { text-decoration: underline; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 8d855ce99b0..c9846103762 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -20,6 +20,9 @@ $l-cyan: #8abeb7; $l-white: $ci-text-color; + .term-bold { + font-weight: bold; + } .term-italic { font-style: italic; } 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 70a2275592b..bd4ba384b29 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -24,8 +24,8 @@ 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 :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?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -97,12 +97,8 @@ class ApplicationController < ActionController::Base current_application_settings.after_sign_out_path.presence || new_user_session_path end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def access_denied! @@ -250,10 +246,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/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index a7af3cb8345..e06d12cfce1 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -7,19 +7,14 @@ module Ci def create @content = params[:content] + @error = Ci::GitlabCiYamlProcessor.validation_message(@content) + @status = @error.blank? - if @content.blank? - @status = false - @error = "Please provide content of .gitlab-ci.yml" - else + if @error.blank? @config_processor = Ci::GitlabCiYamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds - @status = true end - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e - @error = e.message - @status = false rescue @error = 'Undefined error' @status = false diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index ba07cea569c..d5a8a962662 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -62,6 +62,7 @@ module AuthenticatesWithTwoFactor session.delete(:otp_user_id) session.delete(:challenges) + remember_me(user) if user_params[:remember_me] == '1' sign_in(user) else flash.now[:alert] = 'Authentication via U2F device failed.' diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index f40b62446e5..bb32bc502e6 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -3,21 +3,54 @@ module IssuableActions included do before_action :authorize_destroy_issuable!, only: :destroy + before_action :authorize_admin_issuable!, only: :bulk_update end def destroy issuable.destroy + destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym + TodoService.new.public_send(destroy_method, issuable, current_user) name = issuable.class.name.titleize.downcase flash[:notice] = "The #{name} was successfully deleted." redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) end + def bulk_update + result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name) + quantity = result[:count] + + render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } + end + private def authorize_destroy_issuable! - unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable) + unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) return access_denied! end end + + def authorize_admin_issuable! + unless can?(current_user, :"admin_#{resource_name}", @project) + return access_denied! + end + end + + def bulk_update_params + params.require(:update).permit( + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] + ) + end + + def resource_name + @resource_name ||= controller_name.singularize + end end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index a69877edfd4..4cb3be41064 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -13,7 +13,7 @@ module ServiceParams # `issue_events` and `merge_request_events` (singular!) # See app/helpers/services_helper.rb for how we # make those event names plural as special case. - :issues_events, :merge_requests_events, + :issues_events, :confidential_issues_events, :merge_requests_events, :notify_only_broken_builds, :notify_only_broken_pipelines, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, 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/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7e8597a5eb3..256c41e6145 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,18 +1,17 @@ class Import::BaseController < ApplicationController private - def get_or_create_namespace + def find_or_create_namespace(name, owner) + return current_user.namespace if name == owner + return current_user.namespace unless current_user.can_create_group? + begin - namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user) + name = params[:target_namespace].presence || name + namespace = Group.create!(name: name, path: name, owner: current_user) namespace.add_owner(current_user) + namespace rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid - namespace = Namespace.find_by_path_or_name(@target_namespace) - unless current_user.can?(:create_projects, namespace) - @already_been_taken = true - return false - end + Namespace.find_by_path_or_name(name) end - - namespace end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 944c73d139a..6ea54744da8 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -35,23 +35,20 @@ class Import::BitbucketController < Import::BaseController end def create - @repo_id = params[:repo_id] || "" - repo = client.project(@repo_id.gsub("___", "/")) - @project_name = repo["slug"] - - repo_owner = repo["owner"] - repo_owner = current_user.username if repo_owner == client.user["user"]["username"] - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) + @repo_id = params[:repo_id].to_s + repo = client.project(@repo_id.gsub('___', '/')) + @project_name = repo['slug'] + @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username']) unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute - @access_denied = true - render - return + render 'deploy_key' and return end - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 9c1b0eb20f4..8c6bdd16383 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -41,14 +41,13 @@ class Import::GithubController < Import::BaseController @repo_id = params[:repo_id].to_i repo = client.repo(@repo_id) @project_name = repo.name + @target_namespace = find_or_create_namespace(repo.owner.login, client.user.login) - repo_owner = repo.owner.login - repo_owner = current_user.username if repo_owner == client.user.login - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 08130ee8176..73837ffbe67 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -26,15 +26,14 @@ class Import::GitlabController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.project(@repo_id) - @project_name = repo["name"] + @project_name = repo['name'] + @target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username']) - repo_owner = repo["namespace"]["path"] - repo_owner = current_user.username if repo_owner == client.user["username"] - @target_namespace = params[:new_namespace].presence || repo_owner - - namespace = get_or_create_namespace || (render and return) - - @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute + if current_user.can?(:create_projects, @target_namespace) + @project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute + else + render 'unauthorized' + end end private 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/jwt_controller.rb b/app/controllers/jwt_controller.rb index 014b9b43ff2..66ebdcc37a7 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -37,7 +37,7 @@ class JwtController < ApplicationController def authenticate_project(login, password) if login == 'gitlab-ci-token' - Project.find_by(builds_enabled: true, runners_token: password) + Project.with_builds_enabled.find_by(runners_token: password) end end diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index 5a94dcb0dbd..83eec1bf4a2 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -14,7 +14,7 @@ class NamespacesController < ApplicationController if user redirect_to user_path(user) - elsif group && can?(current_user, :read_group, namespace) + elsif group && can?(current_user, :read_group, group) redirect_to group_path(group) elsif current_user.nil? authenticate_user! diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 91315a07deb..b2ff36f6538 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -88,6 +88,6 @@ class Projects::ApplicationController < ApplicationController end def builds_enabled - return render_404 unless @project.builds_enabled? + return render_404 unless @project.feature_available?(:builds, current_user) 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 1a4f6b50e8f..9404612a993 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -8,12 +8,15 @@ module Projects issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = issues.page(params[:page]) - render json: issues.as_json( - only: [:iid, :title, :confidential], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } - }) + render json: { + issues: issues.as_json( + only: [:iid, :title, :confidential], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + }), + size: issues.total_count + } end def update diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 12195c3cbb8..77934ff9962 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -78,8 +78,8 @@ class Projects::BuildsController < Projects::ApplicationController end def raw - if @build.has_trace? - send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline' + if @build.has_trace_file? + send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline' else render_404 end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index b2e8733ccb7..d174e1145a7 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -38,6 +38,6 @@ class Projects::DiscussionsController < Projects::ApplicationController end def module_enabled - render_404 unless @project.merge_requests_enabled + render_404 unless @project.feature_available?(:merge_requests, current_user) end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index a5b4031c30f..f5ce63fdfed 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -117,4 +117,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController def ci? @ci.present? end + + def verify_workhorse_api! + Gitlab::Workhorse.verify_api_request!(request.headers) + end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index b4373ef89ef..9805705c4e3 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,6 +1,8 @@ # This file should be identical in GitLab Community Edition and Enterprise Edition class Projects::GitHttpController < Projects::GitHttpClientController + before_action :verify_workhorse_api! + # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) def info_refs @@ -56,6 +58,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def render_ok + set_workhorse_internal_api_content_type render json: Gitlab::Workhorse.git_http_ok(repository, user) end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index b5624046387..0ae8ff98009 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -59,6 +59,7 @@ class Projects::HooksController < Projects::ApplicationController :pipeline_events, :enable_ssl_verification, :issues_events, + :confidential_issues_events, :merge_requests_events, :note_events, :push_events, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7b0189150f8..de02e28e384 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -20,9 +20,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow modify issue before_action :authorize_update_issue!, only: [:edit, :update] - # Allow issues bulk update - before_action :authorize_admin_issues!, only: [:bulk_update] - respond_to :html def index @@ -168,16 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController end end - def bulk_update - result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute - - respond_to do |format| - format.json do - render json: { notice: "#{result[:count]} issues updated" } - end - end - end - protected def issue @@ -201,7 +188,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled && @project.default_issues_tracker? + return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? end def redirect_to_external_issue_tracker @@ -212,7 +199,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 @@ -237,17 +224,4 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end - - def bulk_update_params - params.require(:update).permit( - :issues_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ) - end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 0ca675623e5..28fa4a5b141 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -99,7 +99,7 @@ class Projects::LabelsController < Projects::ApplicationController protected def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 69066cb40e6..9005b104e90 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -3,6 +3,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController before_action :require_lfs_enabled! before_action :lfs_check_access! + before_action :verify_workhorse_api!, only: [:upload_authorize] def download lfs_object = LfsObject.find_by_oid(oid) @@ -15,14 +16,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController end def upload_authorize - render( - json: { - StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", - LfsOid: oid, - LfsSize: size, - }, - content_type: 'application/json; charset=utf-8' - ) + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.lfs_upload_ok(oid, size) end def upload_finalize diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6a8c7166b39..8895cb955bd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -83,12 +83,33 @@ 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 + + @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } + + if params[:start_sha].present? + @start_sha = params[:start_sha] + @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } + + unless @start_version + render_404 + end + end respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diffs(diff_options) + if @start_sha + compared_diff_version + else + original_diff_version + end render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -403,7 +424,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.merge_requests_enabled + return render_404 unless @project.feature_available?(:merge_requests, current_user) end def validates_merge_request @@ -519,4 +540,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute end + + def compared_diff_version + @diff_notes_disabled = true + @diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options) + end + + def original_diff_version + @diff_notes_disabled = !@merge_request_diff.latest? + @diffs = @merge_request_diff.diffs(diff_options) + end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da2892bfb3f..ff63f22cb5b 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -106,7 +106,7 @@ class Projects::MilestonesController < Projects::ApplicationController end def module_enabled - unless @project.issues_enabled || @project.merge_requests_enabled + unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user) return render_404 end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index b0c72cfe4b4..371cc3787fb 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - all_pipelines = project.pipelines - @pipelines_count = all_pipelines.count - @running_or_pending_count = all_pipelines.running_or_pending.count - @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) - @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30) + @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) + + @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count + @pipelines_count = PipelinesFinder.new(project).execute.count end def new diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 6a227d85f6f..97e6e9471e0 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -20,9 +20,8 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params[:service]) redirect_to( - edit_namespace_project_service_path(@project.namespace, @project, - @service.to_param, notice: - 'Successfully updated.') + edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), + notice: 'Successfully updated.' ) else render 'edit' diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 6d0a7ee1031..17ceefec3b8 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -94,7 +94,7 @@ class Projects::SnippetsController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.snippets_enabled + return render_404 unless @project.feature_available?(:snippets, current_user) end def snippet_params diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 8592579abbd..6ea8ee62bc5 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,4 +1,6 @@ class Projects::TagsController < Projects::ApplicationController + include SortingHelper + # Authorize before_action :require_non_empty_project before_action :authorize_download_code! @@ -6,8 +8,10 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_admin_project!, only: [:destroy] def index - @sort = params[:sort] || 'name' - @tags = @repository.tags_sorted_by(@sort) + params[:sort] = params[:sort].presence || 'name' + + @sort = params[:sort] + @tags = TagsFinder.new(@repository, params).execute @tags = Kaminari.paginate_array(@tags).page(params[:page]) @releases = project.releases.where(tag: @tags.map(&:name)) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fc52cd2f367..eaa38fa6c98 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -303,13 +303,23 @@ class ProjectsController < Projects::ApplicationController end def project_params + project_feature_attributes = + { + project_feature_attributes: + [ + :issues_access_level, :builds_access_level, + :wiki_access_level, :merge_requests_access_level, :snippets_access_level + ] + } + params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :container_registry_enabled, :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 + :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, + :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, + :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, + :lfs_enabled, project_feature_attributes ) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 33daac0399e..60996b181f2 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -64,7 +64,7 @@ class IssuableFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index 641fbf838f1..32aea75486d 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -1,30 +1,34 @@ class PipelinesFinder - attr_reader :project + attr_reader :project, :pipelines def initialize(project) @project = project + @pipelines = project.pipelines end - def execute(pipelines, scope) - case scope - when 'running' - pipelines.running_or_pending - when 'branches' - from_ids(pipelines, ids_for_ref(pipelines, branches)) - when 'tags' - from_ids(pipelines, ids_for_ref(pipelines, tags)) - else - pipelines - end + def execute(scope: nil) + scoped_pipelines = + case scope + when 'running' + pipelines.running_or_pending + when 'branches' + from_ids(ids_for_ref(branches)) + when 'tags' + from_ids(ids_for_ref(tags)) + else + pipelines + end + + scoped_pipelines.order(id: :desc) end private - def ids_for_ref(pipelines, refs) + def ids_for_ref(refs) pipelines.where(ref: refs).group(:ref).select('max(id)') end - def from_ids(pipelines, ids) + def from_ids(ids) pipelines.unscoped.where(id: ids) end diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb new file mode 100644 index 00000000000..b474f0805dc --- /dev/null +++ b/app/finders/tags_finder.rb @@ -0,0 +1,29 @@ +class TagsFinder + def initialize(repository, params) + @repository = repository + @params = params + end + + def execute + tags = @repository.tags_sorted_by(sort) + filter_by_name(tags) + end + + private + + def sort + @params[:sort].presence + end + + def search + @params[:search].presence + end + + def filter_by_name(tags) + if search + tags.select { |tag| tag.name.include?(search) } + else + tags + end + end +end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 06b3e8a9502..a93a63bdb9b 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -83,7 +83,7 @@ class TodosFinder if project? @project = Project.find(params[:project_id]) - unless Ability.abilities.allowed?(current_user, :read_project, @project) + unless Ability.allowed?(current_user, :read_project, @project) @project = nil end else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3733b01721..5f3765cad0d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,7 +110,7 @@ module ApplicationHelper project = event.project # Skip if project repo is empty or MR disabled - return false unless project && !project.empty_repo? && project.merge_requests_enabled + return false unless project && !project.empty_repo? && project.feature_available?(:merge_requests, current_user) # Skip if user already created appropriate MR return false if project.merge_requests.where(source_branch: event.branch_name).opened.any? diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index aa8acbe7567..df41473543b 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -14,7 +14,8 @@ module AvatarsHelper avatar_icon(options[:user] || options[:user_email], avatar_size), class: "avatar has-tooltip hidden-xs s#{avatar_size}", alt: "#{user_name}'s avatar", - title: user_name + title: user_name, + data: { container: 'body' } ) if options[:user] diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index bb285a17baf..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 @@ -41,7 +46,7 @@ module CiStatusHelper when 'play' '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/compare_helper.rb b/app/helpers/compare_helper.rb index f1dc906cab4..aa54ee07bdc 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - project.merge_requests_enabled && + project.feature_available?(:merge_requests, current_user) && project.repository.branch_names.include?(from) && project.repository.branch_names.include?(to) end diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb index 09684955233..8ab394384f3 100644 --- a/app/helpers/git_helper.rb +++ b/app/helpers/git_helper.rb @@ -2,4 +2,8 @@ module GitHelper def strip_gpg_signature(text) text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") end + + def short_sha(text) + Commit.truncate_sha(text) + end end 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/import_helper.rb b/app/helpers/import_helper.rb index 109bc1a02d1..021d2b14718 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,4 +1,9 @@ module ImportHelper + def import_project_target(owner, name) + namespace = current_user.can_create_group? ? owner : current_user.namespace_path + "#{namespace}/#{name}" + end + def github_project_link(path_with_namespace) link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b9baeb1d6c4..5c04bba323f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -49,6 +49,19 @@ module IssuablesHelper end end + def project_dropdown_label(project_id, default_label) + return default_label if project_id.nil? + return "Any project" if project_id == "0" + + project = Project.find_by(id: project_id) + + if project + project.name_with_namespace + else + default_label + end + end + def milestone_dropdown_label(milestone_title, default_label = "Milestone") if milestone_title == Milestone::Upcoming.name milestone_title = Milestone::Upcoming.title 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..8abe7865fed 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -98,6 +98,16 @@ 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 + + def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil) + diffs_namespace_project_merge_request_path( + project.namespace, project, merge_request, + diff_id: merge_request_diff.id, start_sha: start_sha) + end + + def version_index(merge_request_diff) + @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff) end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6c1cc6ef072..df87fac132d 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,21 +1,7 @@ module NavHelper - def nav_menu_collapsed? - cookies[:collapsed_nav] == 'true' - end - - def nav_sidebar_class - if nav_menu_collapsed? - "sidebar-collapsed" - else - "sidebar-expanded" - end - end - def page_sidebar_class if pinned_nav? "page-sidebar-expanded page-sidebar-pinned" - else - "page-sidebar-collapsed" end end @@ -25,6 +11,7 @@ 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" @@ -41,9 +28,7 @@ module NavHelper class_name << " with-horizontal-nav" if defined?(nav) && nav if pinned_nav? - class_name << " header-expanded header-pinned-nav" - else - class_name << " header-collapsed" + class_name << " header-sidebar-expanded header-sidebar-pinned" end class_name diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 356f27f2d5d..16a8e52a4ca 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -61,7 +61,9 @@ module ProjectsHelper project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } if current_user - project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) + project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do + icon("chevron-down") + end end full_title = "#{namespace_link} / #{project_link}".html_safe @@ -127,6 +129,19 @@ module ProjectsHelper current_user.recent_push(project_ids) end + def project_feature_access_select(field) + # Don't show option "everyone with access" if project is private + options = project_feature_options + + if @project.private? + options.delete('Everyone with access') + highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED + end + + options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) + content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe + end + private def get_project_nav_tabs(project, current_user) @@ -187,6 +202,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 @@ -400,4 +427,12 @@ module ProjectsHelper message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end + + def project_feature_options + { + 'Disabled' => ProjectFeature::DISABLED, + 'Only team members' => ProjectFeature::PRIVATE, + 'Everyone with access' => ProjectFeature::ENABLED + } + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c0195713f4a..e523c46e879 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -7,8 +7,10 @@ module SearchHelper projects_autocomplete(term) ].flatten + search_pattern = Regexp.new(Regexp.escape(term), "i") + generic_results = project_autocomplete + default_autocomplete + help_autocomplete - generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") } + generic_results.select! { |result| result[:label] =~ search_pattern } [ resources_results, @@ -44,7 +46,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 index f8cccade15b..3d255df66a0 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -1,27 +1,9 @@ module SentryHelper def sentry_enabled? - Rails.env.production? && current_application_settings.sentry_enabled? + Gitlab::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 + Gitlab::Sentry.context(current_user) end end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 2dd0bf5d71e..3d4abf76419 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -8,7 +8,9 @@ module ServicesHelper when "note" "Event will be triggered when someone adds a comment" when "issue" - "Event will be triggered when an issue is created/updated/merged" + "Event will be triggered when an issue is created/updated/closed" + when "confidential_issue" + "Event will be triggered when a confidential issue is created/updated/closed" when "merge_request" "Event will be triggered when a merge request is created/updated/merged" when "build" @@ -19,7 +21,7 @@ module ServicesHelper end def service_event_field_name(event) - event = event.pluralize if %w[merge_request issue].include?(event) + event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end end diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb new file mode 100644 index 00000000000..d440edc55ba --- /dev/null +++ b/app/helpers/sidekiq_helper.rb @@ -0,0 +1,19 @@ +module SidekiqHelper + SIDEKIQ_PS_REGEXP = /\A + (?<pid>\d+)\s+ + (?<cpu>[\d\.,]+)\s+ + (?<mem>[\d\.,]+)\s+ + (?<state>[DRSTWXZNLsl\+<]+)\s+ + (?<start>.+)\s+ + (?<command>sidekiq.*\])\s+ + \z/x + + def parse_sidekiq_ps(line) + match = line.match(SIDEKIQ_PS_REGEXP) + if match + match[1..6] + else + %w[? ? ? ? ? ?] + end + end +end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index fb85544df2d..c0ec1634cdb 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -3,6 +3,16 @@ module TagsHelper "/tags/#{tag}" end + def filter_tags_path(options = {}) + exist_opts = { + search: params[:search], + sort: params[:sort] + } + + options = exist_opts.merge(options) + namespace_project_tags_path(@project.namespace, @project, @id, options) + end + def tag_list(project) html = '' project.tag_list.each do |tag| diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 0465327060e..1e86f648203 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -78,13 +78,11 @@ module TodosHelper end def todo_actions_options - actions = [ - OpenStruct.new(id: '', title: 'Any Action'), - OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), - OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') + [ + { id: '', text: 'Any Action' }, + { id: Todo::ASSIGNED, text: 'Assigned' }, + { id: Todo::MENTIONED, text: 'Mentioned' } ] - - options_from_collection_for_select(actions, 'id', 'title', params[:action_id]) end def todo_projects_options @@ -92,22 +90,28 @@ module TodosHelper projects = projects.includes(:namespace) projects = projects.map do |project| - OpenStruct.new(id: project.id, title: project.name_with_namespace) + { id: project.id, text: project.name_with_namespace } end - projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) - - options_from_collection_for_select(projects, 'id', 'title', params[:project_id]) + projects.unshift({ id: '', text: 'Any Project' }).to_json end def todo_types_options - types = [ - OpenStruct.new(title: 'Any Type', name: ''), - OpenStruct.new(title: 'Issue', name: 'Issue'), - OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') + [ + { id: '', text: 'Any Type' }, + { id: 'Issue', text: 'Issue' }, + { id: 'MergeRequest', text: 'Merge Request' } ] + end + + def todo_actions_dropdown_label(selected_action_id, default_action) + selected_action = todo_actions_options.find { |action| action[:id] == selected_action_id.to_i} + selected_action ? selected_action[:text] : default_action + end - options_from_collection_for_select(types, 'name', 'title', params[:type]) + def todo_types_dropdown_label(selected_type, default_type) + selected_type = todo_types_options.find { |type| type[:id] == selected_type && type[:id] != '' } + selected_type ? selected_type[:text] : default_type end private diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index d887cdadc34..88f374be1e5 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -34,4 +34,8 @@ module WorkhorseHelper headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry)) head :ok end + + def set_workhorse_internal_api_content_type + headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + end end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 8b83bbd93b7..61a574d3dc0 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -9,7 +9,7 @@ class BaseMailer < ActionMailer::Base default reply_to: Proc.new { default_reply_to_address.format } def can? - Ability.abilities.allowed?(current_user, action, subject) + Ability.allowed?(current_user, action, subject) end private diff --git a/app/models/ability.rb b/app/models/ability.rb index a49dd703926..fa8f8bc3a5f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,34 +1,5 @@ class Ability class << self - # rubocop: disable Metrics/CyclomaticComplexity - def allowed(user, subject) - return anonymous_abilities(user, subject) if user.nil? - return [] unless user.is_a?(User) - return [] if user.blocked? - - abilities_by_subject_class(user: user, subject: subject) - end - - def abilities_by_subject_class(user:, subject:) - case subject - when CommitStatus then commit_status_abilities(user, subject) - when Project then project_abilities(user, subject) - when Issue then issue_abilities(user, subject) - when Note then note_abilities(user, subject) - when ProjectSnippet then project_snippet_abilities(user, subject) - when PersonalSnippet then personal_snippet_abilities(user, subject) - when MergeRequest then merge_request_abilities(user, subject) - when Group then group_abilities(user, subject) - when Namespace then namespace_abilities(user, subject) - when GroupMember then group_member_abilities(user, subject) - when ProjectMember then project_member_abilities(user, subject) - when User then user_abilities - when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project) - when Ci::Runner then runner_abilities(user, subject) - else [] - end.concat(global_abilities(user)) - end - # Given a list of users and a project this method returns the users that can # read the given project. def users_that_can_read_project(users, project) @@ -61,359 +32,7 @@ class Ability issues.select { |issue| issue.visible_to_user?(user) } end - # List of possible abilities for anonymous user - def anonymous_abilities(user, subject) - if subject.is_a?(PersonalSnippet) - anonymous_personal_snippet_abilities(subject) - elsif subject.is_a?(ProjectSnippet) - anonymous_project_snippet_abilities(subject) - elsif subject.is_a?(CommitStatus) - anonymous_commit_status_abilities(subject) - elsif subject.is_a?(Project) || subject.respond_to?(:project) - anonymous_project_abilities(subject) - elsif subject.is_a?(Group) || subject.respond_to?(:group) - anonymous_group_abilities(subject) - elsif subject.is_a?(User) - anonymous_user_abilities - else - [] - end - end - - def anonymous_project_abilities(subject) - project = if subject.is_a?(Project) - subject - else - subject.project - end - - if project && project.public? - rules = [ - :read_project, - :read_board, - :read_list, - :read_wiki, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :read_pipeline, - :read_commit_status, - :read_container_image, - :download_code - ] - - # Allow to read builds by anonymous user if guests are allowed - rules << :read_build if project.public_builds? - - # Allow to read issues by anonymous user if issue is not confidential - rules << :read_issue unless subject.is_a?(Issue) && subject.confidential? - - rules - project_disabled_features_rules(project) - else - [] - end - end - - def anonymous_commit_status_abilities(subject) - rules = anonymous_project_abilities(subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def anonymous_group_abilities(subject) - rules = [] - - group = if subject.is_a?(Group) - subject - else - subject.group - end - - rules << :read_group if group.public? - - rules - end - - def anonymous_personal_snippet_abilities(snippet) - if snippet.public? - [:read_personal_snippet] - else - [] - end - end - - def anonymous_project_snippet_abilities(snippet) - if snippet.public? - [:read_project_snippet] - else - [] - end - end - - def anonymous_user_abilities - [:read_user] unless restricted_public_level? - end - - def global_abilities(user) - rules = [] - rules << :create_group if user.can_create_group - rules << :read_users_list - rules - end - - def project_abilities(user, project) - key = "/user/#{user.id}/project/#{project.id}" - - if RequestStore.active? - RequestStore.store[key] ||= uncached_project_abilities(user, project) - else - uncached_project_abilities(user, project) - end - end - - def uncached_project_abilities(user, project) - rules = [] - # Push abilities on the users team role - rules.push(*project_team_rules(project.team, user)) - - owner = user.admin? || - project.owner == user || - (project.group && project.group.has_owner?(user)) - - if owner - rules.push(*project_owner_rules) - end - - if project.public? || (project.internal? && !user.external?) - rules.push(*public_project_rules) - - # Allow to read builds for internal projects - rules << :read_build if project.public_builds? - - unless owner || project.team.member?(user) || project_group_member?(project, user) - rules << :request_access if project.request_access_enabled - end - end - - if project.archived? - rules -= project_archived_rules - end - - (rules - project_disabled_features_rules(project)).uniq - end - - def project_team_rules(team, user) - # Rules based on role in project - if team.master?(user) - project_master_rules - elsif team.developer?(user) - project_dev_rules - elsif team.reporter?(user) - project_report_rules - elsif team.guest?(user) - project_guest_rules - else - [] - end - end - - def public_project_rules - @public_project_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :read_commit_status, - :read_pipeline, - :read_container_image - ] - end - - def project_guest_rules - @project_guest_rules ||= [ - :read_project, - :read_wiki, - :read_issue, - :read_board, - :read_list, - :read_label, - :read_milestone, - :read_project_snippet, - :read_project_member, - :read_merge_request, - :read_note, - :create_project, - :create_issue, - :create_note, - :upload_file - ] - end - - def project_report_rules - @project_report_rules ||= project_guest_rules + [ - :download_code, - :fork_project, - :create_project_snippet, - :update_issue, - :admin_issue, - :admin_label, - :admin_list, - :read_commit_status, - :read_build, - :read_container_image, - :read_pipeline, - :read_environment, - :read_deployment - ] - end - - def project_dev_rules - @project_dev_rules ||= project_report_rules + [ - :admin_merge_request, - :update_merge_request, - :create_commit_status, - :update_commit_status, - :create_build, - :update_build, - :create_pipeline, - :update_pipeline, - :create_merge_request, - :create_wiki, - :push_code, - :resolve_note, - :create_container_image, - :update_container_image, - :create_environment, - :create_deployment - ] - end - - def project_archived_rules - @project_archived_rules ||= [ - :create_merge_request, - :push_code, - :push_code_to_protected_branches, - :update_merge_request, - :admin_merge_request - ] - end - - def project_master_rules - @project_master_rules ||= project_dev_rules + [ - :push_code_to_protected_branches, - :update_project_snippet, - :update_environment, - :update_deployment, - :admin_milestone, - :admin_project_snippet, - :admin_project_member, - :admin_merge_request, - :admin_note, - :admin_wiki, - :admin_project, - :admin_commit_status, - :admin_build, - :admin_container_image, - :admin_pipeline, - :admin_environment, - :admin_deployment - ] - end - - def project_owner_rules - @project_owner_rules ||= project_master_rules + [ - :change_namespace, - :change_visibility_level, - :rename_project, - :remove_project, - :archive_project, - :remove_fork_project, - :destroy_merge_request, - :destroy_issue - ] - end - - def project_disabled_features_rules(project) - rules = [] - - unless project.issues_enabled - rules += named_abilities('issue') - end - - unless project.merge_requests_enabled - rules += named_abilities('merge_request') - end - - unless project.issues_enabled or project.merge_requests_enabled - rules += named_abilities('label') - rules += named_abilities('milestone') - end - - unless project.snippets_enabled - rules += named_abilities('project_snippet') - end - - unless project.wiki_enabled - rules += named_abilities('wiki') - end - - unless project.builds_enabled - rules += named_abilities('build') - rules += named_abilities('pipeline') - rules += named_abilities('environment') - rules += named_abilities('deployment') - end - - unless project.container_registry_enabled - rules += named_abilities('container_image') - end - - rules - end - - def group_abilities(user, group) - rules = [] - rules << :read_group if can_read_group?(user, group) - - owner = user.admin? || group.has_owner?(user) - master = owner || group.has_master?(user) - - # Only group masters and group owners can create new projects - if master - rules += [ - :create_projects, - :admin_milestones - ] - end - - # Only group owner and administrators can admin group - if owner - rules += [ - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level - ] - end - - if group.public? || (group.internal? && !user.external?) - rules << :request_access if group.request_access_enabled && group.users.exclude?(user) - end - - rules.flatten - end - - def can_read_group?(user, group) - return true if user.admin? - return true if group.public? - return true if group.internal? && !user.external? - return true if group.users.include?(user) - - GroupProjectsFinder.new(group).execute(user).any? - end - + # TODO: make this private and use the actual abilities stuff for this def can_edit_note?(user, note) return false if !note.editable? || !user.present? return true if note.author == user || user.admin? @@ -426,207 +45,23 @@ class Ability end end - def namespace_abilities(user, namespace) - rules = [] - - # Only namespace owner and administrators can admin it - if namespace.owner == user || user.admin? - rules += [ - :create_projects, - :admin_namespace - ] - end - - rules.flatten - end - - [:issue, :merge_request].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] - - if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user) - rules += [ - :"read_#{name}", - :"update_#{name}", - ] - end - - rules += project_abilities(user, subject.project) - rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue) - rules - end - end - - def note_abilities(user, note) - rules = [] - - if note.author == user - rules += [ - :read_note, - :update_note, - :admin_note, - :resolve_note - ] - end - - if note.respond_to?(:project) && note.project - rules += project_abilities(user, note.project) - end - - if note.for_merge_request? && note.noteable.author == user - rules << :resolve_note - end - - rules - end - - def personal_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user - rules += [ - :read_personal_snippet, - :update_personal_snippet, - :admin_personal_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) - rules << :read_personal_snippet - end - - rules + def allowed?(user, action, subject) + allowed(user, subject).include?(action) end - def project_snippet_abilities(user, snippet) - rules = [] - - if snippet.author == user || user.admin? - rules += [ - :read_project_snippet, - :update_project_snippet, - :admin_project_snippet - ] - end - - if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) - rules << :read_project_snippet - end - - rules - end - - def group_member_abilities(user, subject) - rules = [] - target_user = subject.user - group = subject.group - - unless group.last_owner?(target_user) - can_manage = group_abilities(user, group).include?(:admin_group_member) - - if can_manage - rules << :update_group_member - rules << :destroy_group_member - elsif user == target_user - rules << :destroy_group_member - end - end - - rules - end - - def project_member_abilities(user, subject) - rules = [] - target_user = subject.user - project = subject.project - - unless target_user == project.owner - can_manage = project_abilities(user, project).include?(:admin_project_member) - - if can_manage - rules << :update_project_member - rules << :destroy_project_member - elsif user == target_user - rules << :destroy_project_member - end - end - - rules - end - - def commit_status_abilities(user, subject) - rules = project_abilities(user, subject.project) - # If subject is Ci::Build which inherits from CommitStatus filter the abilities - rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) - rules - end - - def filter_build_abilities(rules) - # If we can't read build we should also not have that - # ability when looking at this in context of commit_status - %w(read create update admin).each do |rule| - rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build") - end - rules - end - - def runner_abilities(user, runner) - if user.is_admin? - [:assign_runner] - elsif runner.is_shared? || runner.locked? - [] - elsif user.ci_authorized_runners.include?(runner) - [:assign_runner] - else - [] - end - end + def allowed(user, subject) + return uncached_allowed(user, subject) unless RequestStore.active? - def user_abilities - [:read_user] - end - - def abilities - @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + user_key = user ? user.id : 'anonymous' + subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global' + key = "/ability/#{user_key}/#{subject_key}" + RequestStore[key] ||= uncached_allowed(user, subject).freeze end private - def restricted_public_level? - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end - - def named_abilities(name) - [ - :"read_#{name}", - :"create_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end - - def filter_confidential_issues_abilities(user, issue, rules) - return rules if user.admin? || !issue.confidential? - - unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER) - rules.delete(:admin_issue) - rules.delete(:read_issue) - rules.delete(:update_issue) - end - - rules - end - - def project_group_member?(project, user) - project.group && - ( - project.group.members.exists?(user_id: user.id) || - project.group.requesters.exists?(user_id: user.id) - ) + def uncached_allowed(user, subject) + BasePolicy.class_for(subject).abilities(user, subject) end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f0bcb2d7cda..55d2e07de08 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -146,7 +146,7 @@ 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: Gitlab::ImportSources.values, shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 755edadb7a4..0bc005af75e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -211,22 +211,31 @@ module Ci end end + def has_trace_file? + File.exist?(path_to_trace) || has_old_trace_file? + end + def has_trace? raw_trace.present? end def raw_trace - if File.file?(path_to_trace) - File.read(path_to_trace) - elsif project.ci_id && File.file?(old_path_to_trace) - # Temporary fix for build trace data integrity - File.read(old_path_to_trace) + if File.exist?(trace_file_path) + File.read(trace_file_path) else # backward compatibility read_attribute :trace end end + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + def has_old_trace_file? + project.ci_id && File.exist?(old_path_to_trace) + end + def trace trace = raw_trace if project && trace.present? && project.runners_token.present? @@ -265,6 +274,14 @@ module Ci end end + def trace_file_path + if has_old_trace_file? + old_path_to_trace + else + path_to_trace + end + end + def dir_to_trace File.join( Settings.gitlab_ci.builds_path, @@ -355,7 +372,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 087abe4cbb1..0b1df9f4294 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) @@ -83,7 +83,7 @@ module Ci 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 @@ -257,8 +257,17 @@ module Ci ] end + def queued_duration + return unless started_at + + seconds = (started_at - created_at).to_i + seconds unless seconds.zero? + end + def update_duration - self.duration = calculate_duration + return unless started_at + + self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self) end def execute_hooks diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 49f05f881a2..ed5d4b13b7e 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,7 +2,7 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model - LAST_CONTACT_TIME = 5.minutes.ago + LAST_CONTACT_TIME = 2.hours.ago AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] diff --git a/app/models/commit.rb b/app/models/commit.rb index 817d063e4a2..e64fd1e0c1b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -108,15 +108,6 @@ class Commit @diff_line_count end - # Returns a string describing the commit for use in a link title - # - # Example - # - # "Commit: Alex Denisov - Project git clone panel" - def link_title - "Commit: #{author_name} - #{title}" - end - # Returns the commits title. # # Usually, the commit title is the first line of the commit message. diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 630ee9601e0..656a242c265 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -4,12 +4,10 @@ # # range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false -# range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # # range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true -# range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # @@ -109,11 +107,6 @@ class CommitRange reference end - # Returns a String for use in a link's title attribute - def reference_title - "Commits #{sha_start} through #{sha_to}" - end - # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 84ceeac7d3e..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' @@ -25,6 +25,8 @@ class CommitStatus < ActiveRecord::Base 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 diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 800a16ab246..d8d4575bb4d 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -2,7 +2,7 @@ module Awardable extend ActiveSupport::Concern included do - has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy + has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy if self < Participable # By default we always load award_emoji user association @@ -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/statuseable.rb b/app/models/concerns/has_status.rb index 750f937b724..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] diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 8e11d4f57cf..22231b2e0f0 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -196,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 a881fb83b7f..b8dd27a7afe 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -28,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/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb new file mode 100644 index 00000000000..9216122923e --- /dev/null +++ b/app/models/concerns/project_features_compatibility.rb @@ -0,0 +1,37 @@ +# Makes api V3 compatible with old project features permissions methods +# +# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled +# fields to a new table "project_features", support for the old fields is still needed in the API. + +module ProjectFeaturesCompatibility + extend ActiveSupport::Concern + + def wiki_enabled=(value) + write_feature_attribute(:wiki_access_level, value) + end + + def builds_enabled=(value) + write_feature_attribute(:builds_access_level, value) + end + + def merge_requests_enabled=(value) + write_feature_attribute(:merge_requests_access_level, value) + end + + def issues_enabled=(value) + write_feature_attribute(:issues_access_level, value) + end + + def snippets_enabled=(value) + write_feature_attribute(:snippets_access_level, value) + end + + private + + def write_feature_attribute(field, value) + build_project_feature unless project_feature + + access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + project_feature.update_attribute(field, access_level) + end +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 c8320ff87fa..559b3075905 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -13,6 +13,11 @@ class DiffNote < Note validate :positions_complete validate :verify_supported + # Keep this scope in sync with the logic in `#resolvable?` + scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') } + scope :resolved, -> { resolvable.where.not(resolved_at: nil) } + scope :unresolved, -> { resolvable.where(resolved_at: nil) } + after_initialize :ensure_original_discussion_id before_validation :set_original_position, :update_position, on: :create before_validation :set_line_code, :set_original_discussion_id @@ -25,6 +30,16 @@ class DiffNote < Note def build_discussion_id(noteable_type, noteable_id, position) [super(noteable_type, noteable_id), *position.key].join("-") end + + # This method must be kept in sync with `#resolve!` + def resolve!(current_user) + unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) + end + + # This method must be kept in sync with `#unresolve!` + def unresolve! + resolved.update_all(resolved_at: nil, resolved_by_id: nil) + end end def new_diff_note? @@ -73,6 +88,7 @@ class DiffNote < Note self.position.diff_refs == diff_refs end + # If you update this method remember to also update the scope `resolvable` def resolvable? !system? && for_merge_request? end @@ -83,6 +99,7 @@ class DiffNote < Note self.resolved_at.present? end + # If you update this method remember to also update `.resolve!` def resolve!(current_user) return unless resolvable? return if resolved? @@ -92,6 +109,7 @@ class DiffNote < Note save! end + # If you update this method remember to also update `.unresolve!` def unresolve! return unless resolvable? return unless resolved? @@ -107,10 +125,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 9676bc03470..de06c13481a 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -1,7 +1,7 @@ class Discussion NUMBER_OF_TRUNCATED_DIFF_LINES = 16 - attr_reader :first_note, :last_note, :notes + attr_reader :notes delegate :created_at, :project, @@ -36,8 +36,6 @@ class Discussion end def initialize(notes) - @first_note = notes.first - @last_note = notes.last @notes = notes end @@ -70,17 +68,25 @@ class Discussion end def resolvable? - return @resolvable if defined?(@resolvable) + return @resolvable if @resolvable.present? @resolvable = diff_discussion? && notes.any?(&:resolvable?) end def resolved? - return @resolved if defined?(@resolved) + return @resolved if @resolved.present? @resolved = resolvable? && notes.none?(&:to_be_resolved?) end + def first_note + @first_note ||= @notes.first + end + + def last_note + @last_note ||= @notes.last + end + def resolved_notes notes.select(&:resolved?) end @@ -100,17 +106,13 @@ class Discussion def resolve!(current_user) return unless resolvable? - notes.each do |note| - note.resolve!(current_user) if note.resolvable? - end + update { |notes| notes.resolve!(current_user) } end def unresolve! return unless resolvable? - notes.each do |note| - note.unresolve! if note.resolvable? - end + update { |notes| notes.unresolve! } end def for_target?(target) @@ -118,7 +120,7 @@ class Discussion end def active? - return @active if defined?(@active) + return @active if @active.present? @active = first_note.active? end @@ -174,4 +176,17 @@ class Discussion prev_lines end + + private + + def update + notes_relation = DiffNote.where(id: notes.map(&:id)).fresh + yield(notes_relation) + + # Set the notes array to the updated notes + @notes = notes_relation.to_a + + # Reset the memoized values + @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil + end end diff --git a/app/models/event.rb b/app/models/event.rb index fd736d12359..a0b7b0dc2b5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -65,7 +65,7 @@ class Event < ActiveRecord::Base elsif created_project? true elsif issue? || issue_note? - Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) + Ability.allowed?(user, :read_issue, note? ? note_target : target) else ((merge_request? || note?) && target.present?) || milestone? end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 836a75b0608..c631e7a7df5 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -2,6 +2,7 @@ class ProjectHook < WebHook belongs_to :project scope :issue_hooks, -> { where(issues_events: true) } + scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index f365dee3141..595602e80fe 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base default_value_for :push_events, true default_value_for :issues_events, false + default_value_for :confidential_issues_events, false default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5330a07ee35..b0b1313f94a 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 @@ -238,12 +240,21 @@ class MergeRequest < ActiveRecord::Base def source_branch_head source_branch_ref = @source_branch_sha || source_branch - source_project.repository.commit(source_branch) if source_branch_ref + source_project.repository.commit(source_branch_ref) if source_branch_ref end def target_branch_head target_branch_ref = @target_branch_sha || target_branch - target_project.repository.commit(target_branch) if target_branch_ref + target_project.repository.commit(target_branch_ref) 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 @@ -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( @@ -387,7 +411,7 @@ class MergeRequest < ActiveRecord::Base def can_remove_source_branch?(current_user) !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && - Ability.abilities.allowed?(current_user, :push_code, source_project) && + Ability.allowed?(current_user, :push_code, source_project) && diff_head_commit == source_branch_head end @@ -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..18c583add88 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,64 @@ 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(self.head_commit_sha) + project.commit(head_commit_sha) + end + + def diff_refs + return unless start_commit_sha || base_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 compare - @compare ||= - begin - # Update ref for merge request - merge_request.fetch_ref - - Gitlab::Git::Compare.new( - repository.raw_repository, - self.target_branch_sha, - self.source_branch_sha - ) - end + def diffs(diff_options = nil) + Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options) end - private + def project + merge_request.target_project + end - # Collect array of Git::Commit objects - # between target and source branches - def unmerged_commits - commits = compare.commits + def compare + @compare ||= + Gitlab::Git::Compare.new( + repository.raw_repository, + safe_start_commit_sha, + head_commit_sha + ) + end - if commits.present? - commits = Commit.decorate(commits, merge_request.source_project).reverse - end + def latest? + self == merge_request.merge_request_diff + end - commits + def compare_with(sha) + CompareService.new.execute(project, head_commit_sha, project, sha) end + private + def dump_commits(commits) commits.map(&:to_hash) end @@ -122,26 +166,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 +201,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 +227,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 +272,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 f2656df028b..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 diff --git a/app/models/project.rb b/app/models/project.rb index ec47953799f..64d0675b8e6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -11,24 +11,23 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ProjectFeaturesCompatibility extend Gitlab::ConfigHelper UNKNOWN_IMPORT_URL = 'http://unknown.git' + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level - default_value_for :issues_enabled, gitlab_config_features.issues - default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests - default_value_for :builds_enabled, gitlab_config_features.builds - default_value_for :wiki_enabled, gitlab_config_features.wiki - default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist after_save :ensure_dir_exist, if: :namespace_id_changed? + after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -62,10 +61,10 @@ class Project < ActiveRecord::Base belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :namespace - has_one :board, dependent: :destroy - has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' + has_one :board, dependent: :destroy + # Project services has_many :services has_one :campfire_service, dependent: :destroy @@ -131,6 +130,7 @@ class Project < ActiveRecord::Base has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_one :project_feature, dependent: :destroy has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id @@ -143,6 +143,7 @@ class Project < ActiveRecord::Base has_many :deployments, dependent: :destroy accepts_nested_attributes_for :variables, allow_destroy: true + accepts_nested_attributes_for :project_feature delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -160,8 +161,6 @@ class Project < ActiveRecord::Base length: { within: 0..255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } - validates :issues_enabled, :merge_requests_enabled, - :wiki_enabled, inclusion: { in: [true, false] } validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id @@ -197,6 +196,9 @@ class Project < ActiveRecord::Base scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') } + scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') } + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } @@ -391,6 +393,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 @@ -437,7 +446,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 @@ -681,6 +690,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 @@ -1036,6 +1049,7 @@ class Project < ActiveRecord::Base "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) + repository.expire_avatar_cache(branch) reload_default_branch end @@ -1096,16 +1110,21 @@ 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 - self.builds_enabled = true + project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end def any_runners?(&block) @@ -1272,6 +1291,11 @@ class Project < ActiveRecord::Base private + # Prevents the creation of project_feature record for every project + def setup_project_feature + build_project_feature unless project_feature + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb new file mode 100644 index 00000000000..9c602c582bd --- /dev/null +++ b/app/models/project_feature.rb @@ -0,0 +1,63 @@ +class ProjectFeature < ActiveRecord::Base + # == Project features permissions + # + # Grants access level to project tools + # + # Tools can be enabled only for users, everyone or disabled + # Access control is made only for non private projects + # + # levels: + # + # Disabled: not enabled for anyone + # Private: enabled only for team members + # Enabled: enabled for everyone able to access the project + # + + # Permision levels + DISABLED = 0 + PRIVATE = 10 + ENABLED = 20 + + FEATURES = %i(issues merge_requests wiki snippets builds) + + belongs_to :project + + def feature_available?(feature, user) + raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) + + get_permission(user, public_send("#{feature}_access_level")) + end + + def builds_enabled? + return true unless builds_access_level + + builds_access_level > DISABLED + end + + def wiki_enabled? + return true unless wiki_access_level + + wiki_access_level > DISABLED + end + + def merge_requests_enabled? + return true unless merge_requests_access_level + + merge_requests_access_level > DISABLED + end + + private + + def get_permission(user, level) + case level + when DISABLED + false + when PRIVATE + user && (project.team.member?(user) || user.admin?) + when ENABLED + true + else + true + end + end +end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index d7c986c1a91..afebd3b6a12 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -39,7 +39,7 @@ class HipchatService < Service end def supported_events - %w(push issue merge_request note tag_push build) + %w(push issue confidential_issue merge_request note tag_push build) end def execute(data) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index abbc780dc1a..e1b937817f4 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,6 +1,6 @@ class SlackService < Service prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines validates :webhook, presence: true, url: true, if: :activated? def initialize_properties @@ -10,6 +10,7 @@ class SlackService < Service if properties.nil? self.properties = {} self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true end end @@ -38,13 +39,15 @@ class SlackService < Service { type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'channel', placeholder: "#general" }, { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] default_fields + build_event_channels end def supported_events - %w(push issue merge_request note tag_push build wiki_page) + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] end def execute(data) @@ -62,32 +65,22 @@ class SlackService < Service # 'close' action. Ignore update events for now to prevent duplicate # messages from arriving. - message = \ - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username + message = get_message(object_kind, data) if message + opt = {} + + event_channel = get_channel_field(object_kind) || channel + + opt[:channel] = event_channel if event_channel + opt[:username] = username if username + notifier = Slack::Notifier.new(webhook, opt) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + else + false end end @@ -105,6 +98,25 @@ class SlackService < Service private + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + def get_channel_field(event) field_name = event_channel_name(event) self.public_send(field_name) @@ -142,6 +154,17 @@ class SlackService < Service false end end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end end require "slack_service/issue_message" @@ -149,4 +172,5 @@ require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" require "slack_service/build_message" +require "slack_service/pipeline_message" require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb index 69c21b3fc38..0fca4267bad 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/slack_service/build_message.rb @@ -9,7 +9,7 @@ class SlackService attr_reader :user_name attr_reader :duration - def initialize(params, commit = true) + def initialize(params) @sha = params[:sha] @ref_type = params[:tag] ? 'tag' : 'branch' @ref = params[:ref] @@ -36,7 +36,7 @@ class SlackService def message "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" - end + end def format(string) Slack::Notifier::LinkFormatter.format(string) diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb new file mode 100644 index 00000000000..f06b3562965 --- /dev/null +++ b/app/models/project_services/slack_service/pipeline_message.rb @@ -0,0 +1,79 @@ +class SlackService + class PipelineMessage < BaseMessage + attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url, + :user_name, :duration, :pipeline_id + + def initialize(data) + pipeline_attributes = data[:object_attributes] + @sha = pipeline_attributes[:sha] + @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + @ref = pipeline_attributes[:ref] + @status = pipeline_attributes[:status] + @duration = pipeline_attributes[:duration] + @pipeline_id = pipeline_attributes[:id] + + @project_name = data[:project][:path_with_namespace] + @project_url = data[:project][:web_url] + @user_name = data[:commit] && data[:commit][:author_name] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def pipeline_url + "#{project_url}/pipelines/#{pipeline_id}" + end + + def pipeline_link + "[#{Commit.truncate_sha(sha)}](#{pipeline_url})" + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index bdc3b9d1c1c..7b7090b8a73 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -120,8 +120,21 @@ class Repository commits end - def find_branch(name) - raw_repository.branches.find { |branch| branch.name == name } + def find_branch(name, fresh_repo: true) + # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may + # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate + # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc) + # may cause the branch to "disappear" erroneously or have the wrong SHA. + # + # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 + raw_repo = + if fresh_repo + Gitlab::Git::Repository.new(path_to_repo) + else + raw_repository + end + + raw_repo.find_branch(name) end def find_tag(name) @@ -136,7 +149,7 @@ class Repository return false unless target GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.branches.create(branch_name, target) + update_ref!(ref, target, oldrev) end after_create_branch @@ -168,7 +181,7 @@ class Repository ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - rugged.branches.delete(branch_name) + update_ref!(ref, newrev, oldrev) end after_remove_branch @@ -202,6 +215,21 @@ class Repository rugged.references.exist?(ref) end + def update_ref!(name, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %w[git update-ref --stdin -z] + _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| + stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") + end + + return if status.zero? + + raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") + end + # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -729,7 +757,7 @@ class Repository end def commit_dir(user, path, message, branch) - commit_with_hooks(user, branch) do |ref| + update_branch_with_hooks(user, branch) do |ref| committer = user_to_committer(user) options = {} options[:committer] = committer @@ -746,7 +774,7 @@ class Repository end def commit_file(user, path, content, message, branch, update) - commit_with_hooks(user, branch) do |ref| + update_branch_with_hooks(user, branch) do |ref| committer = user_to_committer(user) options = {} options[:committer] = committer @@ -768,7 +796,7 @@ class Repository end def update_file(user, path, content, branch:, previous_path:, message:) - commit_with_hooks(user, branch) do |ref| + update_branch_with_hooks(user, branch) do |ref| committer = user_to_committer(user) options = {} options[:committer] = committer @@ -795,7 +823,7 @@ class Repository end def remove_file(user, path, message, branch) - commit_with_hooks(user, branch) do |ref| + update_branch_with_hooks(user, branch) do |ref| committer = user_to_committer(user) options = {} options[:committer] = committer @@ -843,7 +871,7 @@ class Repository merge_index = rugged.merge_commits(our_commit, their_commit) return false if merge_index.conflicts? - commit_with_hooks(user, merge_request.target_branch) do + update_branch_with_hooks(user, merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -861,7 +889,7 @@ class Repository return false unless revert_tree_id - commit_with_hooks(user, base_branch) do + update_branch_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.revert_message, @@ -878,7 +906,7 @@ class Repository return false unless cherry_pick_tree_id - commit_with_hooks(user, base_branch) do + update_branch_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.message, @@ -894,7 +922,7 @@ class Repository end def resolve_conflicts(user, branch, params) - commit_with_hooks(user, branch) do + update_branch_with_hooks(user, branch) do committer = user_to_committer(user) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) @@ -998,18 +1026,13 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end - def commit_with_hooks(current_user, branch) + def update_branch_with_hooks(current_user, branch) update_autocrlf_option - oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch target_branch = find_branch(branch) was_empty = empty? - if !was_empty && target_branch - oldrev = target_branch.target.id - end - # Make commit newrev = yield(ref) @@ -1017,24 +1040,19 @@ class Repository raise CommitError.new('Failed to create commit') end + if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? + oldrev = Gitlab::Git::BLANK_SHA + else + oldrev = rugged.merge_base(newrev, target_branch.target.sha) + end + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do + update_ref!(ref, newrev, oldrev) + if was_empty || !target_branch - # Create branch - rugged.references.create(ref, newrev) - # If repo was empty expire cache after_create if was_empty after_create_branch - else - # Update head - current_head = find_branch(branch).target.id - - # Make sure target branch was not changed during pre-receive hook - if current_head == oldrev - rugged.references.update(ref, newrev) - else - raise CommitError.new('Commit was rejected because branch received new push') - end end end @@ -1065,7 +1083,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/service.rb b/app/models/service.rb index 754cca07648..2259e05a4e5 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -7,10 +7,12 @@ class Service < ActiveRecord::Base default_value_for :active, false default_value_for :push_events, true default_value_for :issues_events, true + default_value_for :confidential_issues_events, true default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true + default_value_for :pipeline_events, true default_value_for :wiki_page_events, true after_initialize :initialize_properties @@ -33,6 +35,7 @@ class Service < ActiveRecord::Base scope :push_hooks, -> { where(push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } scope :issue_hooks, -> { where(issues_events: true, active: true) } + scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } @@ -100,7 +103,7 @@ class Service < ActiveRecord::Base end def supported_events - %w(push tag_push issue merge_request wiki_page) + %w(push tag_push issue confidential_issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user.rb b/app/models/user.rb index ad3cfbc03e4..6996740eebd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -433,7 +433,7 @@ class User < ActiveRecord::Base # # This logic is duplicated from `Ability#project_abilities` into a SQL form. def projects_where_can_admin_issues - authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false) + authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end def is_admin? @@ -460,16 +460,12 @@ class User < ActiveRecord::Base can?(:create_group, nil) end - def abilities - Ability.abilities - end - def can_select_namespace? several_namespaces? || admin end def can?(action, subject) - abilities.allowed?(self, action, subject) + Ability.allowed?(self, action, subject) end def first_name diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb new file mode 100644 index 00000000000..118c100ca11 --- /dev/null +++ b/app/policies/base_policy.rb @@ -0,0 +1,116 @@ +class BasePolicy + class RuleSet + attr_reader :can_set, :cannot_set + def initialize(can_set, cannot_set) + @can_set = can_set + @cannot_set = cannot_set + end + + def size + to_set.size + end + + def self.empty + new(Set.new, Set.new) + end + + def can?(ability) + @can_set.include?(ability) && !@cannot_set.include?(ability) + end + + def include?(ability) + can?(ability) + end + + def to_set + @can_set - @cannot_set + end + + def merge(other) + @can_set.merge(other.can_set) + @cannot_set.merge(other.cannot_set) + end + + def can!(*abilities) + @can_set.merge(abilities) + end + + def cannot!(*abilities) + @cannot_set.merge(abilities) + end + + def freeze + @can_set.freeze + @cannot_set.freeze + super + end + end + + def self.abilities(user, subject) + new(user, subject).abilities + end + + def self.class_for(subject) + return GlobalPolicy if subject.nil? + + subject.class.ancestors.each do |klass| + next unless klass.name + + begin + policy_class = "#{klass.name}Policy".constantize + + # NOTE: the < operator here tests whether policy_class + # inherits from BasePolicy + return policy_class if policy_class < BasePolicy + rescue NameError + nil + end + end + + raise "no policy for #{subject.class.name}" + end + + attr_reader :user, :subject + def initialize(user, subject) + @user = user + @subject = subject + end + + def abilities + return RuleSet.empty if @user && @user.blocked? + return anonymous_abilities if @user.nil? + collect_rules { rules } + end + + def anonymous_abilities + collect_rules { anonymous_rules } + end + + def anonymous_rules + rules + end + + def delegate!(new_subject) + @rule_set.merge(Ability.allowed(@user, new_subject)) + end + + def can?(rule) + @rule_set.can?(rule) + end + + def can!(*rules) + @rule_set.can!(*rules) + end + + def cannot!(*rules) + @rule_set.cannot!(*rules) + end + + private + + def collect_rules(&b) + @rule_set = RuleSet.empty + yield + @rule_set + end +end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb new file mode 100644 index 00000000000..2232e231cf8 --- /dev/null +++ b/app/policies/ci/build_policy.rb @@ -0,0 +1,13 @@ +module Ci + class BuildPolicy < CommitStatusPolicy + def rules + super + + # If we can't read build we should also not have that + # ability when looking at this in context of commit_status + %w(read create update admin).each do |rule| + cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" + end + end + end +end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb new file mode 100644 index 00000000000..7edd383530d --- /dev/null +++ b/app/policies/ci/runner_policy.rb @@ -0,0 +1,13 @@ +module Ci + class RunnerPolicy < BasePolicy + def rules + return unless @user + + can! :assign_runner if @user.is_admin? + + return if @subject.is_shared? || @subject.locked? + + can! :assign_runner if @user.ci_authorized_runners.include?(@subject) + end + end +end diff --git a/app/policies/commit_status_policy.rb b/app/policies/commit_status_policy.rb new file mode 100644 index 00000000000..593df738328 --- /dev/null +++ b/app/policies/commit_status_policy.rb @@ -0,0 +1,5 @@ +class CommitStatusPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb new file mode 100644 index 00000000000..163d070ff90 --- /dev/null +++ b/app/policies/deployment_policy.rb @@ -0,0 +1,5 @@ +class DeploymentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb new file mode 100644 index 00000000000..f4219569161 --- /dev/null +++ b/app/policies/environment_policy.rb @@ -0,0 +1,5 @@ +class EnvironmentPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/external_issue_policy.rb b/app/policies/external_issue_policy.rb new file mode 100644 index 00000000000..d9e28bd107a --- /dev/null +++ b/app/policies/external_issue_policy.rb @@ -0,0 +1,5 @@ +class ExternalIssuePolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb new file mode 100644 index 00000000000..3c2fbe6b56b --- /dev/null +++ b/app/policies/global_policy.rb @@ -0,0 +1,8 @@ +class GlobalPolicy < BasePolicy + def rules + return unless @user + + can! :create_group if @user.can_create_group + can! :read_users_list + end +end diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb new file mode 100644 index 00000000000..62335527654 --- /dev/null +++ b/app/policies/group_member_policy.rb @@ -0,0 +1,19 @@ +class GroupMemberPolicy < BasePolicy + def rules + return unless @user + + target_user = @subject.user + group = @subject.group + + return if group.last_owner?(target_user) + + can_manage = Ability.allowed?(@user, :admin_group_member, group) + + if can_manage + can! :update_group_member + can! :destroy_group_member + elsif @user == target_user + can! :destroy_group_member + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb new file mode 100644 index 00000000000..97ff6233968 --- /dev/null +++ b/app/policies/group_policy.rb @@ -0,0 +1,45 @@ +class GroupPolicy < BasePolicy + def rules + can! :read_group if @subject.public? + return unless @user + + globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) + member = @subject.users.include?(@user) + owner = @user.admin? || @subject.has_owner?(@user) + master = owner || @subject.has_master?(@user) + + can_read = false + can_read ||= globally_viewable + can_read ||= member + can_read ||= @user.admin? + can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? + can! :read_group if can_read + + # Only group masters and group owners can create new projects + if master + can! :create_projects + can! :admin_milestones + end + + # Only group owner and administrators can admin group + if owner + can! :admin_group + can! :admin_namespace + can! :admin_group_member + can! :change_visibility_level + end + + if globally_viewable && @subject.request_access_enabled && !member + can! :request_access + end + end + + def can_read_group? + return true if @subject.public? + return true if @user.admin? + return true if @subject.internal? && !@user.external? + return true if @subject.users.include?(@user) + + GroupProjectsFinder.new(@subject).execute(@user).any? + end +end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb new file mode 100644 index 00000000000..c253f9a9399 --- /dev/null +++ b/app/policies/issuable_policy.rb @@ -0,0 +1,14 @@ +class IssuablePolicy < BasePolicy + def action_name + @subject.class.name.underscore + end + + def rules + if @user && (@subject.author == @user || @subject.assignee == @user) + can! :"read_#{action_name}" + can! :"update_#{action_name}" + end + + delegate! @subject.project + end +end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb new file mode 100644 index 00000000000..bd1811a3c54 --- /dev/null +++ b/app/policies/issue_policy.rb @@ -0,0 +1,28 @@ +class IssuePolicy < IssuablePolicy + def issue + @subject + end + + def rules + super + + if @subject.confidential? && !can_read_confidential? + cannot! :read_issue + cannot! :admin_issue + cannot! :update_issue + cannot! :read_issue + end + end + + private + + def can_read_confidential? + return false unless @user + return true if @user.admin? + return true if @subject.author == @user + return true if @subject.assignee == @user + return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER) + + false + end +end diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb new file mode 100644 index 00000000000..bc3afc626fb --- /dev/null +++ b/app/policies/merge_request_policy.rb @@ -0,0 +1,3 @@ +class MergeRequestPolicy < IssuablePolicy + # pass +end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb new file mode 100644 index 00000000000..29bb357e00a --- /dev/null +++ b/app/policies/namespace_policy.rb @@ -0,0 +1,10 @@ +class NamespacePolicy < BasePolicy + def rules + return unless @user + + if @subject.owner == @user || @user.admin? + can! :create_projects + can! :admin_namespace + end + end +end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb new file mode 100644 index 00000000000..83847466ee2 --- /dev/null +++ b/app/policies/note_policy.rb @@ -0,0 +1,19 @@ +class NotePolicy < BasePolicy + def rules + delegate! @subject.project + + return unless @user + + if @subject.author == @user + can! :read_note + can! :update_note + can! :admin_note + can! :resolve_note + end + + if @subject.for_merge_request? && + @subject.noteable.author == @user + can! :resolve_note + end + end +end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb new file mode 100644 index 00000000000..46c5aa1a5be --- /dev/null +++ b/app/policies/personal_snippet_policy.rb @@ -0,0 +1,16 @@ +class PersonalSnippetPolicy < BasePolicy + def rules + can! :read_personal_snippet if @subject.public? + return unless @user + + if @subject.author == @user + can! :read_personal_snippet + can! :update_personal_snippet + can! :admin_personal_snippet + end + + if @subject.internal? && !@user.external? + can! :read_personal_snippet + end + end +end diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb new file mode 100644 index 00000000000..1c038dddd4b --- /dev/null +++ b/app/policies/project_member_policy.rb @@ -0,0 +1,22 @@ +class ProjectMemberPolicy < BasePolicy + def rules + # anonymous users have no abilities here + return unless @user + + target_user = @subject.user + project = @subject.project + + return if target_user == project.owner + + can_manage = Ability.allowed?(@user, :admin_project_member, project) + + if can_manage + can! :update_project_member + can! :destroy_project_member + end + + if @user == target_user + can! :destroy_project_member + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb new file mode 100644 index 00000000000..acf36d422d1 --- /dev/null +++ b/app/policies/project_policy.rb @@ -0,0 +1,224 @@ +class ProjectPolicy < BasePolicy + def rules + team_access!(user) + + owner = user.admin? || + project.owner == user || + (project.group && project.group.has_owner?(user)) + + owner_access! if owner + + if project.public? || (project.internal? && !user.external?) + guest_access! + public_access! + + # Allow to read builds for internal projects + can! :read_build if project.public_builds? + + if project.request_access_enabled && + !(owner || project.team.member?(user) || project_group_member?(user)) + can! :request_access + end + end + + archived_access! if project.archived? + + disabled_features! + end + + def project + @subject + end + + def guest_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_issue + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :create_project + can! :create_issue + can! :create_note + can! :upload_file + end + + def reporter_access! + can! :download_code + can! :fork_project + can! :create_project_snippet + can! :update_issue + can! :admin_issue + can! :admin_label + can! :admin_list + can! :read_commit_status + can! :read_build + can! :read_container_image + can! :read_pipeline + can! :read_environment + can! :read_deployment + end + + def developer_access! + can! :admin_merge_request + can! :update_merge_request + can! :create_commit_status + can! :update_commit_status + can! :create_build + can! :update_build + can! :create_pipeline + can! :update_pipeline + can! :create_merge_request + can! :create_wiki + can! :push_code + can! :resolve_note + can! :create_container_image + can! :update_container_image + can! :create_environment + can! :create_deployment + end + + def master_access! + can! :push_code_to_protected_branches + can! :update_project_snippet + can! :update_environment + can! :update_deployment + can! :admin_milestone + can! :admin_project_snippet + can! :admin_project_member + can! :admin_merge_request + can! :admin_note + can! :admin_wiki + can! :admin_project + can! :admin_commit_status + can! :admin_build + can! :admin_container_image + can! :admin_pipeline + can! :admin_environment + can! :admin_deployment + end + + def public_access! + can! :download_code + can! :fork_project + can! :read_commit_status + can! :read_pipeline + can! :read_container_image + end + + def owner_access! + guest_access! + reporter_access! + developer_access! + master_access! + can! :change_namespace + can! :change_visibility_level + can! :rename_project + can! :remove_project + can! :archive_project + can! :remove_fork_project + can! :destroy_merge_request + can! :destroy_issue + end + + # Push abilities on the users team role + def team_access!(user) + access = project.team.max_member_access(user.id) + + guest_access! if access >= Gitlab::Access::GUEST + reporter_access! if access >= Gitlab::Access::REPORTER + developer_access! if access >= Gitlab::Access::DEVELOPER + master_access! if access >= Gitlab::Access::MASTER + end + + def archived_access! + cannot! :create_merge_request + cannot! :push_code + cannot! :push_code_to_protected_branches + cannot! :update_merge_request + cannot! :admin_merge_request + end + + def disabled_features! + unless project.feature_available?(:issues, user) + cannot!(*named_abilities(:issue)) + end + + unless project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:merge_request)) + end + + unless project.feature_available?(:issues, user) || project.feature_available?(:merge_requests, user) + cannot!(*named_abilities(:label)) + cannot!(*named_abilities(:milestone)) + end + + unless project.feature_available?(:snippets, user) + cannot!(*named_abilities(:project_snippet)) + end + + unless project.feature_available?(:wiki, user) || project.has_external_wiki? + cannot!(*named_abilities(:wiki)) + end + + unless project.feature_available?(:builds, user) + cannot!(*named_abilities(:build)) + cannot!(*named_abilities(:pipeline)) + cannot!(*named_abilities(:environment)) + cannot!(*named_abilities(:deployment)) + end + + unless project.container_registry_enabled + cannot!(*named_abilities(:container_image)) + end + end + + def anonymous_rules + return unless project.public? + + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + + # Allow to read builds by anonymous user if guests are allowed + can! :read_build if project.public_builds? + + disabled_features! + end + + def project_group_member?(user) + project.group && + ( + project.group.members.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end + + def named_abilities(name) + [ + :"read_#{name}", + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}" + ] + end +end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb new file mode 100644 index 00000000000..57acccfafd9 --- /dev/null +++ b/app/policies/project_snippet_policy.rb @@ -0,0 +1,20 @@ +class ProjectSnippetPolicy < BasePolicy + def rules + can! :read_project_snippet if @subject.public? + return unless @user + + if @user && @subject.author == @user || @user.admin? + can! :read_project_snippet + can! :update_project_snippet + can! :admin_project_snippet + end + + if @subject.internal? && !@user.external? + can! :read_project_snippet + end + + if @subject.private? && @subject.project.team.member?(@user) + can! :read_project_snippet + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000000..03a2499e263 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,11 @@ +class UserPolicy < BasePolicy + include Gitlab::CurrentSettings + + def rules + can! :read_user if @user || !restricted_public_level? + end + + def restricted_public_level? + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end +end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 0d55ba5a981..0c208150fb8 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -7,12 +7,8 @@ class BaseService @project, @current_user, @params = project, user, params.dup end - def abilities - Ability.abilities - end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end def notification_service 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..de48a50774e 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,13 +10,15 @@ module Ci create_builds! end - new_builds = - stage_indexes_of_created_builds.map do |index| - process_stage(index) - end + @pipeline.with_lock do + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end - # Return a flag if a when builds got enqueued - new_builds.flatten.any? + # Return a flag if a when builds got enqueued + new_builds.flatten.any? + end end private @@ -34,7 +36,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/register_build_service.rb b/app/services/ci/register_build_service.rb index 9a187f5d694..6973191b203 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,16 +8,18 @@ module Ci builds = if current_runner.shared? builds. - # don't run projects which have not enabled shared runners - joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). + # don't run projects which have not enabled shared runners and builds + joins(:project).where(projects: { shared_runners_enabled: true }). + joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). # this returns builds that are ordered by number of running builds # we prefer projects that don't use shared runners at all joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). + where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') else # do run projects which are only assigned to this runner (FIFO) - builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC') + builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC') end build = builds.find do |build| 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/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb new file mode 100644 index 00000000000..60891cbb255 --- /dev/null +++ b/app/services/issuable/bulk_update_service.rb @@ -0,0 +1,26 @@ +module Issuable + class BulkUpdateService < IssuableBaseService + def execute(type) + model_class = type.classify.constantize + update_class = type.classify.pluralize.constantize::UpdateService + + ids = params.delete(:issuable_ids).split(",") + items = model_class.where(id: ids) + + %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key| + params.delete(key) unless params[key].present? + end + + items.each do |issuable| + next unless can?(current_user, :"update_#{type}", issuable) + + update_class.new(issuable.project, current_user, params).execute(issuable) + end + + { + count: items.count, + success: !items.count.zero? + } + 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/issues/base_service.rb b/app/services/issues/base_service.rb index 089b0f527e2..9ea3ce084ba 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -14,9 +14,10 @@ module Issues end def execute_hooks(issue, action = 'open') - issue_data = hook_data(issue, action) - issue.project.execute_hooks(issue_data, :issue_hooks) - issue.project.execute_services(issue_data, :issue_hooks) + issue_data = hook_data(issue, action) + hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks + issue.project.execute_hooks(issue_data, hooks_scope) + issue.project.execute_services(issue_data, hooks_scope) end end end diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb deleted file mode 100644 index 7e19a73f71a..00000000000 --- a/app/services/issues/bulk_update_service.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Issues - class BulkUpdateService < BaseService - def execute - issues_ids = params.delete(:issues_ids).split(",") - issue_params = params - - %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key| - issue_params.delete(key) unless issue_params[key].present? - end - - issues = Issue.where(id: issues_ids) - - issues.each do |issue| - next unless can?(current_user, :update_issue, issue) - - Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) - end - - { - count: issues.count, - success: !issues.count.zero? - } - end - end -end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 290742f1506..e57791f6818 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -83,7 +83,7 @@ module MergeRequests closes_issue = "Closes ##{iid}" if merge_request.description.present? - merge_request.description += closes_issue.prepend("\n") + merge_request.description += closes_issue.prepend("\n\n") else merge_request.description = closes_issue end diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 08c1f72d65a..1262ecbc29a 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -31,7 +31,7 @@ module MergeRequests def get_branches(changes) return [] if project.empty_repo? - return [] unless project.merge_requests_enabled + return [] unless project.merge_requests_enabled? changes_list = Gitlab::ChangesList.new(changes) changes_list.map do |change| 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/projects/create_service.rb b/app/services/projects/create_service.rb index 55956be2844..be749ba4a1c 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,7 +7,6 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) import_data = params.delete(:import_data) - @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level @@ -81,8 +80,7 @@ module Projects log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") unless @project.gitlab_project_import? - @project.create_wiki if @project.wiki_enabled? - + @project.create_wiki if @project.feature_available?(:wiki, current_user) @project.build_missing_services @project.create_labels diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index de6dc38cc8e..a2de4dccece 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -8,7 +8,6 @@ module Projects name: @project.name, path: @project.path, shared_runners_enabled: @project.shared_runners_enabled, - builds_enabled: @project.builds_enabled, namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } @@ -17,6 +16,9 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute + builds_access_level = @project.project_feature.builds_access_level + new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + new_project end 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..776530ac0a5 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -31,6 +31,14 @@ class TodoService mark_pending_todos_as_done(issue, current_user) end + # When we destroy an issue we should: + # + # * refresh the todos count cache for the current user + # + def destroy_issue(issue, current_user) + destroy_issuable(issue, current_user) + end + # When we reassign an issue we should: # # * create a pending todo for new assignee if issue is assigned @@ -64,6 +72,14 @@ class TodoService mark_pending_todos_as_done(merge_request, current_user) end + # When we destroy a merge request we should: + # + # * refresh the todos count cache for the current user + # + def destroy_merge_request(merge_request, current_user) + destroy_issuable(merge_request, current_user) + end + # When we reassign a merge request we should: # # * creates a pending todo for new assignee if merge request is assigned @@ -148,7 +164,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 @@ -186,6 +203,10 @@ class TodoService create_mention_todos(issuable.project, issuable, author) end + def destroy_issuable(issuable, user) + user.update_todos_count_cache + end + def toggling_tasks?(issuable) issuable.previous_changes.include?('description') && issuable.tasks? && issuable.updated_tasks.any? 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/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/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 4f680b507c4..05855db963a 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -28,14 +28,10 @@ %th COMMAND %tbody - @sidekiq_processes.each do |process| - - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) - - data = process.strip.split(' ') %tr %td= gitlab_config.user - - 5.times do - %td= data.shift - %td= data.join(' ') - + - parse_sidekiq_ps(process).each do |value| + %td= value .clearfix %p %i.fa.fa-exclamation-circle 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 d320d3bcc1e..9d31f31c639 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -28,21 +28,25 @@ .row-content-block.second-block = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do .filter-item.inline - = select_tag('project_id', todo_projects_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Project'}) + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options } }) .filter-item.inline - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) .filter-item.inline - = select_tag('type', todo_types_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Type'}) + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options } }) .filter-item.inline.actions-filter - = select_tag('action_id', todo_actions_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Action'}) - + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options }}) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -66,7 +70,7 @@ - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| - .panel.panel-default.panel-small.js-todos-list + .panel.panel-default.panel-small - project = group[0] .panel-heading = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) @@ -76,11 +80,3 @@ = paginate @todos, theme: "gitlab" - else .nothing-here-block You're all done! - -:javascript - new UsersSelect(); - - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); - }); diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 4debd3d608f..e623f7cff88 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -18,6 +18,5 @@ = f.submit "Verify code", class: "btn btn-save" - if @user.two_factor_u2f_enabled? - %hr - = render "u2f/authenticate" + = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name } diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5c318cd3b8b..31fdcc5e21b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,7 +1,7 @@ - if event.visible_to_user?(current_user) .event-item{ class: event_row_class(event) } .event-item-timestamp - #{time_ago_with_tooltip(event.created_at)} + #{time_ago_with_tooltip(event.created_at, skip_js: true)} = cache [event, current_application_settings, "v2.2"] do = author_avatar(event, size: 40) 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/update.js.haml b/app/views/groups/group_members/update.js.haml index 742f9d7a433..3be7ed8432c 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,3 @@ :plain $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); - new MemberExpirationDate(); + new gl.MemberExpirationDate(); diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index ce4536ebdc6..16c16cec137 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -7,277 +7,278 @@ Keyboard Shortcuts %small = link_to '(Show all)', '#', class: 'js-more-help-button' - .modal-body.shortcuts-cheatsheet - .col-lg-4 - %table.shortcut-mappings - %tbody - %tr - %th - %th Global Shortcuts - %tr - %td.shortcut - .key s - %td Focus Search - %tr - %td.shortcut - .key f - %td Focus Filter - %tr - %td.shortcut - .key ? - %td Show/hide this dialog - %tr - %td.shortcut - - if browser.platform.mac? - .key ⌘ shift p - - else - .key ctrl shift p - %td Toggle Markdown preview - %tr - %td.shortcut - .key - %i.fa.fa-arrow-up - %td Edit last comment (when focused on an empty textarea) - %tbody - %tr - %th - %th Project Files browsing - %tr - %td.shortcut - .key - %i.fa.fa-arrow-up - %td Move selection up - %tr - %td.shortcut - .key - %i.fa.fa-arrow-down - %td Move selection down - %tr - %td.shortcut - .key enter - %td Open Selection - %tbody - %tr - %th - %th Finding Project File - %tr - %td.shortcut - .key - %i.fa.fa-arrow-up - %td Move selection up - %tr - %td.shortcut - .key - %i.fa.fa-arrow-down - %td Move selection down - %tr - %td.shortcut - .key enter - %td Open Selection - %tr - %td.shortcut - .key esc - %td Go back + .modal-body + .row + .col-lg-4 + %table.shortcut-mappings + %tbody + %tr + %th + %th Global Shortcuts + %tr + %td.shortcut + .key s + %td Focus Search + %tr + %td.shortcut + .key f + %td Focus Filter + %tr + %td.shortcut + .key ? + %td Show/hide this dialog + %tr + %td.shortcut + - if browser.platform.mac? + .key ⌘ shift p + - else + .key ctrl shift p + %td Toggle Markdown preview + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Edit last comment (when focused on an empty textarea) + %tbody + %tr + %th + %th Project Files browsing + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tbody + %tr + %th + %th Finding Project File + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + %td Move selection up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + %td Move selection down + %tr + %td.shortcut + .key enter + %td Open Selection + %tr + %td.shortcut + .key esc + %td Go back - .col-lg-4 - %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut project', style: 'display:none' } - %tr - %th - %th Global Dashboard - %tr - %td.shortcut - .key g - .key a - %td - Go to the activity feed - %tr - %td.shortcut - .key g - .key p - %td - Go to projects - %tr - %td.shortcut - .key g - .key i - %td - Go to issues - %tr - %td.shortcut - .key g - .key m - %td - Go to merge requests - %tbody - %tr - %th - %th Project - %tr - %td.shortcut - .key g - .key p - %td - Go to the project's home page - %tr - %td.shortcut - .key g - .key e - %td - Go to the project's activity feed - %tr - %td.shortcut - .key g - .key f - %td - Go to files - %tr - %td.shortcut - .key g - .key c - %td - Go to commits - %tr - %td.shortcut - .key g - .key b - %td - Go to builds - %tr - %td.shortcut - .key g - .key n - %td - Go to network graph - %tr - %td.shortcut - .key g - .key g - %td - Go to graphs - %tr - %td.shortcut - .key g - .key i - %td - Go to issues - %tr - %td.shortcut - .key g - .key m - %td - Go to merge requests - %tr - %td.shortcut - .key g - .key s - %td - Go to snippets - %tr - %td.shortcut - .key t - %td Go to finding file - %tr - %td.shortcut - .key i - %td New issue - .col-lg-4 - %table.shortcut-mappings - %tbody{ class: 'hidden-shortcut network', style: 'display:none' } - %tr - %th - %th Network Graph - %tr - %td.shortcut - .key - %i.fa.fa-arrow-left - \/ - .key h - %td Scroll left - %tr - %td.shortcut - .key - %i.fa.fa-arrow-right - \/ - .key l - %td Scroll right - %tr - %td.shortcut - .key - %i.fa.fa-arrow-up - \/ - .key k - %td Scroll up - %tr - %td.shortcut - .key - %i.fa.fa-arrow-down - \/ - .key j - %td Scroll down - %tr - %td.shortcut - .key - shift - %i.fa.fa-arrow-up - \/ - .key - shift k - %td Scroll to top - %tr - %td.shortcut - .key - shift - %i.fa.fa-arrow-down - \/ - .key - shift j - %td Scroll to bottom - %tbody{ class: 'hidden-shortcut issues', style: 'display:none' } - %tr - %th - %th Issues - %tr - %td.shortcut - .key a - %td Change assignee - %tr - %td.shortcut - .key m - %td Change milestone - %tr - %td.shortcut - .key r - %td Reply (quoting selected text) - %tr - %td.shortcut - .key e - %td Edit issue - %tr - %td.shortcut - .key l - %td Change Label - %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } - %tr - %th - %th Merge Requests - %tr - %td.shortcut - .key a - %td Change assignee - %tr - %td.shortcut - .key m - %td Change milestone - %tr - %td.shortcut - .key r - %td Reply (quoting selected text) - %tr - %td.shortcut - .key e - %td Edit merge request - %tr - %td.shortcut - .key l - %td Change Label + .col-lg-4 + %table.shortcut-mappings + %tbody{ class: 'hidden-shortcut project', style: 'display:none' } + %tr + %th + %th Global Dashboard + %tr + %td.shortcut + .key g + .key a + %td + Go to the activity feed + %tr + %td.shortcut + .key g + .key p + %td + Go to projects + %tr + %td.shortcut + .key g + .key i + %td + Go to issues + %tr + %td.shortcut + .key g + .key m + %td + Go to merge requests + %tbody + %tr + %th + %th Project + %tr + %td.shortcut + .key g + .key p + %td + Go to the project's home page + %tr + %td.shortcut + .key g + .key e + %td + Go to the project's activity feed + %tr + %td.shortcut + .key g + .key f + %td + Go to files + %tr + %td.shortcut + .key g + .key c + %td + Go to commits + %tr + %td.shortcut + .key g + .key b + %td + Go to builds + %tr + %td.shortcut + .key g + .key n + %td + Go to network graph + %tr + %td.shortcut + .key g + .key g + %td + Go to graphs + %tr + %td.shortcut + .key g + .key i + %td + Go to issues + %tr + %td.shortcut + .key g + .key m + %td + Go to merge requests + %tr + %td.shortcut + .key g + .key s + %td + Go to snippets + %tr + %td.shortcut + .key t + %td Go to finding file + %tr + %td.shortcut + .key i + %td New issue + .col-lg-4 + %table.shortcut-mappings + %tbody{ class: 'hidden-shortcut network', style: 'display:none' } + %tr + %th + %th Network Graph + %tr + %td.shortcut + .key + %i.fa.fa-arrow-left + \/ + .key h + %td Scroll left + %tr + %td.shortcut + .key + %i.fa.fa-arrow-right + \/ + .key l + %td Scroll right + %tr + %td.shortcut + .key + %i.fa.fa-arrow-up + \/ + .key k + %td Scroll up + %tr + %td.shortcut + .key + %i.fa.fa-arrow-down + \/ + .key j + %td Scroll down + %tr + %td.shortcut + .key + shift + %i.fa.fa-arrow-up + \/ + .key + shift k + %td Scroll to top + %tr + %td.shortcut + .key + shift + %i.fa.fa-arrow-down + \/ + .key + shift j + %td Scroll to bottom + %tbody{ class: 'hidden-shortcut issues', style: 'display:none' } + %tr + %th + %th Issues + %tr + %td.shortcut + .key a + %td Change assignee + %tr + %td.shortcut + .key m + %td Change milestone + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) + %tr + %td.shortcut + .key e + %td Edit issue + %tr + %td.shortcut + .key l + %td Change Label + %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } + %tr + %th + %th Merge Requests + %tr + %td.shortcut + .key a + %td Change assignee + %tr + %td.shortcut + .key m + %td Change milestone + %tr + %td.shortcut + .key r + %td Reply (quoting selected text) + %tr + %td.shortcut + .key e + %td Edit merge request + %tr + %td.shortcut + .key l + %td Change Label 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/base/create.js.haml b/app/views/import/base/create.js.haml index 804ad88468f..8e929538351 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -1,23 +1,4 @@ -- if @already_been_taken - :plain - tr = $("tr#repo_#{@repo_id}") - target_field = tr.find(".import-target") - import_button = tr.find(".btn-import") - origin_target = target_field.text() - project_name = "#{@project_name}" - origin_namespace = "#{@target_namespace}" - target_field.empty() - target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>") - target_field.append("<input type='text' name='target_namespace' />") - target_field.append("/" + project_name) - target_field.data("project_name", project_name) - target_field.find('input').prop("value", origin_namespace) - import_button.enable().removeClass('is-loading') -- elsif @access_denied - :plain - job = $("tr#repo_#{@repo_id}") - job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") -- elsif @project.persisted? +- if @project.persisted? :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml new file mode 100644 index 00000000000..36f8069c1f7 --- /dev/null +++ b/app/views/import/base/unauthorized.js.haml @@ -0,0 +1,14 @@ +:plain + tr = $("tr#repo_#{@repo_id}") + target_field = tr.find(".import-target") + import_button = tr.find(".btn-import") + origin_target = target_field.text() + project_name = "#{@project_name}" + origin_namespace = "#{@target_namespace.path}" + target_field.empty() + target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>") + target_field.append("<input type='text' name='target_namespace' />") + target_field.append("/" + project_name) + target_field.data("project_name", project_name) + target_field.find('input').prop("value", origin_namespace) + import_button.enable().removeClass('is-loading') diff --git a/app/views/import/bitbucket/deploy_key.js.haml b/app/views/import/bitbucket/deploy_key.js.haml new file mode 100644 index 00000000000..81b34ab5c9d --- /dev/null +++ b/app/views/import/bitbucket/deploy_key.js.haml @@ -0,0 +1,3 @@ +:plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 15dd98077c8..f8b4b107513 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -51,7 +51,7 @@ %td = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" %td.import-target - = "#{repo["owner"]}/#{repo["slug"]}" + = import_project_target(repo['owner'], repo['slug']) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 54ff1d27c67..bd3be20c4f8 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -45,7 +45,7 @@ %td = github_project_link(repo.full_name) %td.import-target - = repo.full_name + = import_project_target(repo.owner.login, repo.name) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index fcfc6fd37f4..d31fc2e6adb 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -45,7 +45,7 @@ %td = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" %td.import-target - = repo["path_with_namespace"] + = import_project_target(repo['namespace']['path'], repo['name']) %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import 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/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index bf50633af24..4f7839a881f 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,5 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } + .sidebar-wrapper.nicescroll .sidebar-action-buttons = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do %span.sr-only Toggle navigation diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index d7d36c84b6c..27ac1760166 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/group_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/group_settings' .fade-left = icon('angle-left') .fade-right diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index bf9a7ecb786..75275afc0f3 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,22 +1,26 @@ - if current_user + - can_admin_group = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group) - member = @group.members.find_by(user_id: current_user.id) - can_leave = member && can?(current_user, :destroy_group_member, member) - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - %li.divider - - if can_edit - %li - = link_to 'Edit Group', edit_group_path(@group) - - if can_leave - %li - = link_to polymorphic_path([:leave, @group, :members]), - data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do - Leave Group + - if can_admin_group || can_edit || can_leave + .controls + .dropdown.group-settings-dropdown + %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - if can_admin_group + = nav_link(path: 'groups#projects') do + = link_to 'Projects', projects_group_path(@group), title: 'Projects' + - if can_edit || can_leave + %li.divider + - if can_edit + %li + = link_to 'Edit Group', edit_group_path(@group) + - if can_leave + %li + = link_to polymorphic_path([:leave, @group, :members]), + data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do + Leave Group diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 52a5bdc1a1b..613b8b7d301 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -26,7 +26,7 @@ %span Protected Branches - - if @project.builds_enabled? + - if @project.feature_available?(:builds, current_user) = nav_link(controller: :runners) do = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do %span diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 19b4249374b..80053dd501b 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,11 +1,14 @@ -%fieldset.builds-feature - %h5.prepend-top-0 - Merge Requests - .form-group - .checkbox - = f.label :only_allow_merge_if_build_succeeds do - = f.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds - .help-block - Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') +.merge-requests-feature + %fieldset.builds-feature + %hr + %h5.prepend-top-0 + Merge Requests + .form-group + .checkbox + = f.label :only_allow_merge_if_build_succeeds do + = f.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + %br + %span.descr + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 377665b096f..5a98e258b22 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -11,7 +11,7 @@ %small= number_to_human_size @blob.size .file-actions = render "projects/blob/actions" - .file-content.blame.code.js-syntax-highlight + .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 - @blame_groups.each do |blame_group| @@ -19,6 +19,7 @@ %td.blame-commit .commit - commit = blame_group[:commit] + = author_avatar(commit, size: 36) .commit-row-title %strong = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index de53a298f84..73066150fb3 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -13,19 +13,13 @@ %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } {{ list.title }} %span.pull-right{ "v-if" => "list.type !== 'blank'" } - {{ list.issues.length }} + {{ list.issuesSize }} - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", "v-if" => "!list.preset && list.id" } %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", @@ -39,5 +33,11 @@ "v-show" => "!loading", ":data-board" => "list.id" } = render "projects/boards/components/card" + %li.board-list-count.text-center{ "v-if" => "showCount" } + = icon("spinner spin", "v-show" => "list.loadingMore" ) + %span{ "v-if" => "list.issues.length === list.issuesSize" } + Showing all issues + %span{ "v-else" => true } + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 6192ccb710b..5217b8bf028 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -3,6 +3,7 @@ - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] +- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do @@ -19,14 +20,16 @@ %i.fa.fa-lock protected .controls.hidden-xs - - if create_mr_button?(@repository.root_ref, branch.name) + - if merge_project && create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do Merge Request - if branch.name != @repository.root_ref - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", 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/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 5b0b58e087b..56306b05934 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,3 +1,6 @@ +- builds = @build.pipeline.builds.latest.to_a +- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] + %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Build @@ -11,40 +14,6 @@ %p.build-detail-row #{@build.coverage}% - - builds = @build.pipeline.builds.latest.to_a - - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] - - if builds.size > 1 - .dropdown.build-dropdown - .build-light-text Stage - %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} - %span.stage-selection More - = icon('caret-down') - %ul.dropdown-menu - - builds.map(&:stage).uniq.each do |stage| - %li - %a.stage-item= stage - - .builds-container - - statuses.each do |build_status| - - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: ('active' if build == @build), data: {stage: build.stage}} - = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('check') - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - - if @build.retried? - %li.active - %a - Build ##{@build.id} - · - %i.fa.fa-warning - This build was retried. - .blocks-container - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } @@ -76,7 +45,7 @@ .title Build details - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: @@ -100,7 +69,7 @@ - elsif @build.runner \##{@build.runner.id} .btn-group.btn-group-justified{ role: :group } - - if @build.has_trace? + - if @build.has_trace_file? = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' - if @build.active? = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post @@ -121,12 +90,13 @@ - if @build.trigger_request.variables %p - %span.build-light-text Variables: + %button.btn.group.btn-group-justified.reveal-variables Reveal Variables - @build.trigger_request.variables.each do |key, value| - %code - #{key}=#{value} + .hide.js-build + .js-build-variable= key + .js-build-value= value .block .title @@ -141,3 +111,35 @@ - @build.tag_list.each do |tag| %span.label.label-primary = tag + + - if builds.size > 1 + .dropdown.build-dropdown + .title Stage + %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %span.stage-selection More + = icon('caret-down') + %ul.dropdown-menu + - builds.map(&:stage).uniq.each do |stage| + %li + %a.stage-item= stage + + .builds-container + - statuses.each do |build_status| + - builds.select{|build| build.status == build_status}.each do |build| + .build-job{class: ('active' if build == @build), data: {stage: build.stage}} + = link_to namespace_project_build_path(@project.namespace, @project, build) do + = icon('check') + = ci_icon_for_status(build.status) + %span + - if build.name + = build.name + - else + = build.id + + - if @build.retried? + %li.active + %a + Build ##{@build.id} + · + %i.fa.fa-warning + This build was retried. 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/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index d78888e9fe4..22db33498f1 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -3,11 +3,11 @@ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do = custom_icon('icon_fork') - Fork + %span Fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = custom_icon('icon_fork') - Fork + %span Fork %div.count-with-arrow %span.arrow = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 1fdf32466f2..73de8abe55b 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -89,4 +89,4 @@ = icon('repeat') - elsif build.playable? = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do - = icon('play') + = custom_icon('icon_play') 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 b119f6edf14..bb9493f5158 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -66,13 +66,13 @@ - if actions.any? .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= build.name.humanize - if artifacts.present? .btn-group 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/_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/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index fd888f41b1e..389477d0927 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -7,7 +7,7 @@ - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key.push(commit.status) if commit.status -= cache(cache_key) do += cache(cache_key, expires_in: 1.day) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } = author_avatar(commit, size: 36) 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/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f7bf3b834ef..16d134eb6b6 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -5,13 +5,13 @@ .inline .dropdown %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = icon("play") + = custom_icon('icon_play') %b.caret %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = icon("play") + = custom_icon('icon_play') %span= action.name.humanize - if local_assigns.fetch(:allow_rollback, false) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b282aa52b25..f6d751a343e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -44,42 +44,56 @@ %hr %fieldset.features.append-bottom-0 %h5.prepend-top-0 - Features - .form-group - .checkbox - = f.label :issues_enabled do - = f.check_box :issues_enabled - %strong Issues - %br - %span.descr Lightweight issue tracking system for this project - .form-group - .checkbox - = f.label :merge_requests_enabled do - = f.check_box :merge_requests_enabled - %strong Merge Requests - %br - %span.descr Submit changes to be merged upstream - .form-group - .checkbox - = f.label :builds_enabled do - = f.check_box :builds_enabled - %strong Builds - %br - %span.descr Test and deploy your changes before merge - .form-group - .checkbox - = f.label :wiki_enabled do - = f.check_box :wiki_enabled - %strong Wiki - %br - %span.descr Pages for project documentation - .form-group - .checkbox - = f.label :snippets_enabled do - = f.check_box :snippets_enabled - %strong Snippets - %br - %span.descr Share code pastes with others out of git repository + Feature Visibility + + = f.fields_for :project_feature do |feature_fields| + .form_group.prepend-top-20 + .row + .col-md-9 + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) + + .row + .col-md-9 + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) + + .row + .col-md-9 + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit Test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) + + .row + .col-md-9 + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-3 + = project_feature_access_select(:wiki_access_level) + + .row + .col-md-9 + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) + + - 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 @@ -88,7 +102,7 @@ %strong Container Registry %br %span.descr Enable Container Registry for this repository - %hr + = render 'merge_request_settings', f: f %hr %fieldset.features.append-bottom-default diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index a1d79bdabda..bacc5708e4b 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -32,11 +32,11 @@ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do = custom_icon('icon_fork') - Fork + %span Fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do = custom_icon('icon_fork') - Fork + %span Fork = render 'projects', projects: @forks 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..576d0bec51b 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 @@ -1,9 +1,10 @@ %li.build + .curve .build-content - 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..082e2cb4d8c 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.feature_available?(:builds, current_user) + = nav_link(action: :ci) do + = link_to ci_namespace_project_graph_path do + Continuous Integration diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 3fcf1692e09..ceabe2eab3d 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| + - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 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/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 79b14819865..851d4c06990 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,7 +1,7 @@ %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } - - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) + - if @bulk_edit .issue-check - = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" + = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" .issue-title.title %span.issue-title-text diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index f34f3c05737..a2c31c0b4c5 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,4 +1,4 @@ -%ul.content-list.issues-list +%ul.content-list.issues-list.issuable-list = render @issues - if @issues.blank? %li diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 24749699c6d..c56b6cc11f5 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,13 +1,12 @@ - if can?(current_user, :push_code, @project) .pull-right - #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} + #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} + = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do + = icon('spinner spin') + Checking branches = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), - method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do - .checking - = icon('spinner spin') - Checking branches - .available.hide - New branch - .unavailable.hide - = icon('exclamation-triangle') - New branch unavailable + method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do + New branch + = link_to '#', class: 'unavailable btn btn-grouped hide', disabled: 'disabled' do + = icon('exclamation-triangle') + New branch unavailable 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/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 1a87045aa60..8da9f2100e9 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @bulk_edit = can?(current_user, :admin_issue, @project) + - page_title "Issues" - new_issue_email = @project.new_issue_address(current_user) = render "projects/issues/head" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5029b365f93..31f8d0aeb5b 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,8 @@ %li{ class: mr_css_classes(merge_request) } + - if @bulk_edit + .issue-check + = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" + .merge-request-title.title %span.merge-request-title-text = link_to merge_request.title, merge_request_path(merge_request) diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 446887774a4..fe82f751f53 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -1,4 +1,4 @@ -%ul.content-list.mr-list +%ul.content-list.mr-list.issuable-list = render @merge_requests - if @merge_requests.blank? %li diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9d8b4cc56be..4b4d418e8ec 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -83,7 +83,7 @@ .tab-content#diff-notes-app #notes.notes.tab-pane.voting_notes - .content-block.content-block-small.oneline-block + .content-block.content-block-small = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .row diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index ace275c689b..144b3a9c8c8 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @bulk_edit = can?(current_user, :admin_merge_request, @project) + - page_title "Merge Requests" = render "projects/issues/head" = render 'projects/last_push' 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/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index b727efaa6a6..f1d5441f9dd 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -12,7 +12,7 @@ %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve - git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch} + git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch} git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD - else :preserve @@ -47,8 +47,9 @@ Note that pushing to GitLab requires write access to this repository. %p %strong Tip: - You can also checkout merge requests locally by - %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines + = succeed '.' do + You can also checkout merge requests locally by + = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank' :javascript $(function(){ diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 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..00287f2d245 --- /dev/null +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -0,0 +1,60 @@ +- if @merge_request_diffs.size > 1 + .mr-version-controls + Changes between + %span.dropdown.inline.mr-version-dropdown + %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} } + %strong + - if @merge_request_diff.latest? + latest version + - else + version #{version_index(@merge_request_diff)} + %span.caret + %ul.dropdown-menu.dropdown-menu-selectable + - @merge_request_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %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) + + - if @merge_request_diff.base_commit_sha + and + %span.dropdown.inline.mr-version-compare-dropdown + %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} } + %strong + - if @start_sha + version #{version_index(@start_version)} + - else + #{@merge_request.target_branch} + %span.caret + %ul.dropdown-menu.dropdown-menu-selectable + - @comparable_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %small + = time_ago_with_tooltip(merge_request_diff.created_at) + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do + %strong + #{@merge_request.target_branch} (base) + .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + + - unless @merge_request_diff.latest? && !@start_sha + .prepend-top-10 + = icon('info-circle') + - if @start_sha + Comments are disabled because you're comparing two versions of this merge request. + - else + Comments are disabled because you're viewing an old version of this merge request. diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index ea4898f2107..fda0592dd41 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -55,16 +55,11 @@ = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do = icon('gitlab', text: 'GitLab.com') - 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/_note.html.haml b/app/views/projects/notes/_note.html.haml index d2ac1ce2b9a..9ec17cf6e76 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,6 +1,5 @@ - return unless note.author - return if note.cross_reference_not_visible_for?(current_user) -- can_resolve = can?(current_user, :resolve_note, note) - note_editable = note_editable?(note) %li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } @@ -24,6 +23,8 @@ %span.note-role.hidden-xs= access - if note.resolvable? + - can_resolve = can?(current_user, :resolve_note, note) + %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'", ":project-path" => "'#{note.project.path}'", ":discussion-id" => "'#{note.discussion_id}'", @@ -52,11 +53,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 063e83a407a..5800ef7de48 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -10,6 +10,8 @@ - if @pipeline.duration in = time_interval_in_words(@pipeline.duration) + - if @pipeline.queued_duration + = "(queued for #{time_interval_in_words(@pipeline.queued_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/update.js.haml b/app/views/projects/project_members/update.js.haml index 833954bc039..37e55dc72a3 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,3 @@ :plain $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); - new MemberExpirationDate(); + new gl.MemberExpirationDate(); diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 8ee2aef0e61..1141168f037 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -5,8 +5,8 @@ :plain var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}"); - row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); - row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); + row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); + row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); - if @more_log_url :plain 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 340e159c874..9adce776c1c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -72,7 +72,7 @@ = 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 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/index.html.haml b/app/views/projects/tags/index.html.haml index 368231e73fe..6adbe9351dc 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -8,21 +8,24 @@ Tags give the ability to mark specific points in history as being important .nav-controls - - if can? current_user, :push_code, @project - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - New tag + = form_tag(filter_tags_path, method: :get) do + = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } - %span.light= @sort.humanize + %span.light + = @sort.humanize %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to namespace_project_tags_path(sort: nil) do + = link_to filter_tags_path(sort: nil) do Name - = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do + = link_to filter_tags_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do + = link_to filter_tags_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + - if can?(current_user, :push_code, @project) + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + New tag .tags - if @tags.any? 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/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index a3a4dba3fa4..ee417b58cbf 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -4,6 +4,6 @@ - file_name = blob_item.name = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do %span.str-truncated= file_name - %td.tree_time_ago.cgray - = render 'projects/tree/spinner' - %td.hidden-xs.tree_commit + %td.hidden-xs.tree-commit + %td.tree-time-ago.cgray.text-right + = render 'projects/tree/spinner'
\ No newline at end of file diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 558e6146ae9..0f7d629ab98 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -4,7 +4,6 @@ %thead %tr %th Name - %th Last Update %th.hidden-xs .pull-left Last Commit .last-commit.hidden-sm.pull-left @@ -14,9 +13,11 @@ %small.light = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – - = truncate(@commit.title, length: 50) - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - + = time_ago_with_tooltip(@commit.committed_date) + = @commit.full_title + %small.commit-history-link-spacer | + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link' + %th.text-right Last Update - if @path.present? %tr.tree-item %td.tree-item-file-name diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 9577696fc0d..1ccef6d52ab 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -4,6 +4,6 @@ - path = flatten_tree(tree_item) = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do %span.str-truncated= path - %td.tree_time_ago.cgray - = render 'projects/tree/spinner' - %td.hidden-xs.tree_commit + %td.hidden-xs.tree-commit + %td.tree-time-ago.text-right + = render 'projects/tree/spinner'
\ No newline at end of file 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/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 7f3de47d7df..f6e0b0a7c8a 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -4,65 +4,89 @@ .col-lg-3 %h4.prepend-top-0 = page_title - %p - Triggers can force a specific branch or tag to rebuild with an API call. + %p.prepend-top-20 + Triggers can force a specific branch or tag to get rebuilt with an API call. + %p.append-bottom-0 + = succeed '.' do + Learn more in the + = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' .col-lg-9 - %h5.prepend-top-0 - Your triggers - - if @triggers.any? - .table-responsive - %table.table - %thead - %th Token - %th Last used - %th - = render partial: 'trigger', collection: @triggers, as: :trigger - - else - %p.settings-message.text-center.append-bottom-default - No triggers have been created yet. Add one using the button below. + .panel.panel-default + .panel-heading + %h4.panel-title + Manage your project's triggers + .panel-body + - if @triggers.any? + .table-responsive + %table.table + %thead + %th + %strong Token + %th + %strong Last used + %th + = render partial: 'trigger', collection: @triggers, as: :trigger + - else + %p.settings-message.text-center.append-bottom-default + No triggers have been created yet. Add one using the button below. - = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| - = f.submit "Add Trigger", class: 'btn btn-success' + = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| + = f.submit "Add trigger", class: 'btn btn-success' - %h5.prepend-top-default - Use CURL + .panel-footer - %p.light - Copy the token above, set your branch or tag name, and that reference will be rebuilt. + %p + In the following examples, you can see the exact API call you need to + make in order to rebuild a specific + %code ref + (branch or tag) with a trigger token. + %p + All you need to do is replace the + %code TOKEN + and + %code REF_NAME + with the trigger token and the branch or tag name respectively. - %pre - :plain - curl -X POST \ - -F token=TOKEN \ - -F ref=REF_NAME \ - #{builds_trigger_url(@project.id)} - %h5.prepend-top-default - Use .gitlab-ci.yml + %h5.prepend-top-default + Use cURL - %p.light - In the - %code .gitlab-ci.yml - of the dependent project, include the following snippet. - The project will rebuild at the end of the build. + %p.light + Copy one of the tokens above, set your branch or tag name, and that + reference will be rebuilt. - %pre - :plain - trigger: - type: deploy - script: - - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" - %h5.prepend-top-default - Pass build variables + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + -F ref=REF_NAME \ + #{builds_trigger_url(@project.id)} + %h5.prepend-top-default + Use .gitlab-ci.yml - %p.light - Add - %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + %p.light + In the + %code .gitlab-ci.yml + of another project, include the following snippet. + The project will be rebuilt at the end of the build. - %pre.append-bottom-0 - :plain - curl -X POST \ - -F token=TOKEN \ - -F "ref=REF_NAME" \ - -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{builds_trigger_url(@project.id)} + %pre + :plain + trigger_build: + stage: deploy + script: + - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" + %h5.prepend-top-default + Pass build variables + + %p.light + Add + %code variables[VARIABLE]=VALUE + to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + + %pre.append-bottom-0 + :plain + curl -X POST \ + -F token=TOKEN \ + -F "ref=REF_NAME" \ + -F "variables[RUN_NIGHTLY_BUILD]=true" \ + #{builds_trigger_url(@project.id)} 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/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg index 80a6d41dbf6..e965afa9a56 100644 --- a/app/views/shared/icons/_icon_play.svg +++ b/app/views/shared/icons/_icon_play.svg @@ -1 +1,3 @@ -<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 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"> + <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..93c4d5c3d30 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,9 +1,11 @@ +- boards_page = controller.controller_name == 'boards' + .issues-filters .issues-details-filters.row-content-block.second-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do - if params[:issue_search].present? = hidden_field_tag :issue_search, params[:issue_search] - - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) + - if @bulk_edit .check-all-holder = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" @@ -26,22 +28,28 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" + .filter-item.inline.reset-filters + %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters + .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 boards_page + #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' - - if controller.controller_name == 'issues' + - if @bulk_edit .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do .filter-item.inline = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul @@ -64,10 +72,10 @@ %li %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe - = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline - = button_tag "Update issues", class: "btn update_selected_issues btn-save" + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - if !@labels.nil? .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 22594b46443..3856a4917b4 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -134,7 +134,7 @@ 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 @@ -175,7 +175,7 @@ = 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' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index d34d28f6736..24a1a616919 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -12,7 +12,7 @@ - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| - = hidden_field_tag "label_name[]", label, id: nil + = hidden_field_tag "label_name[]", u(label), id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1b50e65af5..b13daaf43c9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -118,7 +118,7 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } - if issuable.labels_array.any? - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index d2ec6c3ddef..5d659eb83a9 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -52,6 +52,13 @@ %p.light This URL will be triggered when an issue is created/updated/merged %li + = f.check_box :confidential_issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :confidential_issues_events, class: 'list-label' do + %strong Confidential Issues events + %p.light + This URL will be triggered when a confidential issue is created/updated/merged + %li = f.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = f.label :merge_requests_events, class: 'list-label' do diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 80a3e731e1d..7be4a471579 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,7 +1,11 @@ -%ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets - - if @snippets.empty? - %li - .nothing-here-block Nothing here. +.snippets-list-holder + %ul.content-list + = render partial: 'shared/snippets/snippet', collection: @snippets + - if @snippets.empty? + %li + .nothing-here-block Nothing here. -= paginate @snippets, theme: 'gitlab' + = paginate @snippets, theme: 'gitlab', remote: true + +:javascript + gl.SnippetsList(); diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 75fb0e303ad..9657101ace5 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -20,6 +20,8 @@ %div %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. = form_tag(new_user_session_path, method: :post) do |f| + - resource_params = params[resource_name].presence || params + = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0) = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" = submit_tag "Authenticate via U2F Device", class: "btn btn-success" diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c7f39868e71..9a052abe40a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -123,6 +123,6 @@ :javascript var userProfile; - userProfile = new User({ + userProfile = new gl.User({ action: "#{controller.action_name}" }); diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb new file mode 100644 index 00000000000..5883cafe1d1 --- /dev/null +++ b/app/workers/prune_old_events_worker.rb @@ -0,0 +1,17 @@ +class PruneOldEventsWorker + include Sidekiq::Worker + + def perform + # Contribution calendar shows maximum 12 months of events. + # Double nested query is used because MySQL doesn't allow DELETE subqueries + # on the same table. + Event.unscoped.where( + '(id IN (SELECT id FROM (?) ids_to_remove))', + Event.unscoped.where( + 'created_at < ?', + (12.months + 1.day).ago). + select(:id). + limit(10_000)). + delete_all + end +end diff --git a/changelogs/archive.md b/changelogs/archive.md new file mode 100644 index 00000000000..c68ab694d39 --- /dev/null +++ b/changelogs/archive.md @@ -0,0 +1,1810 @@ +## 7.14.3 + +- No changes + +## 7.14.2 + +- Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) +- Allow configuration of LDAP attributes GitLab will use for the new user account. + +## 7.14.1 + +- Improve abuse reports management from admin area +- Fix "Reload with full diff" URL button in compare branch view (Stan Hu) +- Disabled DNS lookups for SSH in docker image (Rowan Wookey) +- Only include base URL in OmniAuth full_host parameter (Stan Hu) +- Fix Error 500 in API when accessing a group that has an avatar (Stan Hu) +- Ability to enable SSL verification for Webhooks + +## 7.14.0 + +- Fix bug where non-project members of the target project could set labels on new merge requests. +- Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller) +- Fix redirection after sign in when using auto_sign_in_with_provider +- Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu) +- Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu) +- Provide more feedback what went wrong if HipChat service failed test (Stan Hu) +- Fix bug where backslashes in inline diffs could be dropped (Stan Hu) +- Disable turbolinks when linking to Bitbucket import status (Stan Hu) +- Fix broken code import and display error messages if something went wrong with creating project (Stan Hu) +- Fix corrupted binary files when using API files endpoint (Stan Hu) +- Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu) +- Show incompatible projects in Bitbucket import status (Stan Hu) +- Fix coloring of diffs on MR Discussion-tab (Gert Goet) +- Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu) +- Fix errors deleting and creating branches with encoded slashes (Stan Hu) +- Always add current user to autocomplete controller to support filter by "Me" (Stan Hu) +- Fix multi-line syntax highlighting (Stan Hu) +- Fix network graph when branch name has single quotes (Stan Hu) +- Add "Confirm user" button in user admin page (Stan Hu) +- Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) +- Add support for Unicode filenames in relative links (Hiroyuki Sato) +- Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) +- Fix commit data retrieval when branch name has single quotes (Stan Hu) +- Check that project was actually created rather than just validated in import:repos task (Stan Hu) +- Fix full screen mode for snippet comments (Daniel Gerhardt) +- Fix 404 error in files view after deleting the last file in a repository (Stan Hu) +- Fix the "Reload with full diff" URL button (Stan Hu) +- Fix label read access for unauthenticated users (Daniel Gerhardt) +- Fix access to disabled features for unauthenticated users (Daniel Gerhardt) +- Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu) +- Fix file upload dialog for comment editing (Daniel Gerhardt) +- Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) +- Return comments in created order in merge request API (Stan Hu) +- Disable internal issue tracker controller if external tracker is used (Stan Hu) +- Expire Rails cache entries after two weeks to prevent endless Redis growth +- Add support for destroying project milestones (Stan Hu) +- Allow custom backup archive permissions +- Add project star and fork count, group avatar URL and user/group web URL attributes to API +- Show who last edited a comment if it wasn't the original author +- Send notification to all participants when MR is merged. +- Add ability to manage user email addresses via the API. +- Show buttons to add license, changelog and contribution guide if they're missing. +- Tweak project page buttons. +- Disabled autocapitalize and autocorrect on login field (Daryl Chan) +- Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) +- Update gravatar link on profile page to link to configured gravatar host (Ben Bodenmiller) +- Remove redis-store TTL monkey patch +- Add support for CI skipped status +- Fetch code from forks to refs/merge-requests/:id/head when merge request created +- Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg) +- Add "Check out branch" button to the MR page. +- Improve MR merge widget text and UI consistency. +- Improve text in MR "How To Merge" modal. +- Cache all events +- Order commits by date when comparing branches +- Fix bug causing error when the target branch of a symbolic ref was deleted +- Include branch/tag name in archive file and directory name +- Add dropzone upload progress +- Add a label for merged branches on branches page (Florent Baldino) +- Detect .mkd and .mkdn files as markdown (Ben Boeckel) +- Fix: User search feature in admin area does not respect filters +- Set max-width for README, issue and merge request description for easier read on big screens +- Update Flowdock integration to support new Flowdock API (Boyan Tabakov) +- Remove author from files view (Sven Strickroth) +- Fix infinite loop when SAML was incorrectly configured. + +## 7.13.5 + +- Satellites reverted + +## 7.13.4 + +- Allow users to send abuse reports + +## 7.13.3 + +- Fix bug causing Bitbucket importer to crash when OAuth application had been removed. +- Allow users to send abuse reports +- Remove satellites +- Link username to profile on Group Members page (Tom Webster) + +## 7.13.2 + +- Fix randomly failed spec +- Create project services on Project creation +- Add admin_merge_request ability to Developer level and up +- Fix Error 500 when browsing projects with no HEAD (Stan Hu) +- Fix labels / assignee / milestone for the merge requests when issues are disabled +- Show the first tab automatically on MergeRequests#new +- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt) +- Fix Gmail Actions + +## 7.13.1 + +- Fix: Label modifications are not reflected in existing notes and in the issue list +- Fix: Label not shown in the Issue list, although it's set through web interface +- Fix: Group/project references are linked incorrectly +- Improve documentation +- Fix of migration: Check if session_expire_delay column exists before adding the column +- Fix: ActionView::Template::Error +- Fix: "Create Merge Request" isn't always shown in event for newly pushed branch +- Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. +- Render Note field hints consistently for "new" and "edit" forms + +## 7.13.0 + +- Remove repository graph log to fix slow cache updates after push event (Stan Hu) +- Only enable HSTS header for HTTPS and port 443 (Stan Hu) +- Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu) +- Fix redirection to home page URL for unauthorized users (Daniel Gerhardt) +- Add branch switching support for graphs (Daniel Gerhardt) +- Fix external issue tracker hook/test for HTTPS URLs (Daniel Gerhardt) +- Remove link leading to a 404 error in Deploy Keys page (Stan Hu) +- Add support for unlocking users in admin settings (Stan Hu) +- Add Irker service configuration options (Stan Hu) +- Fix order of issues imported from GitHub (Hiroyuki Sato) +- Bump rugments to 1.0.0beta8 to fix C prototype function highlighting (Jonathon Reinhart) +- Fix Merge Request webhook to properly fire "merge" action when accepted from the web UI +- Add `two_factor_enabled` field to admin user API (Stan Hu) +- Fix invalid timestamps in RSS feeds (Rowan Wookey) +- Fix downloading of patches on public merge requests when user logged out (Stan Hu) +- Fix Error 500 when relative submodule resolves to a namespace that has a different name from its path (Stan Hu) +- Extract the longest-matching ref from a commit path when multiple matches occur (Stan Hu) +- Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) +- Support commenting on diffs in side-by-side mode (Stan Hu) +- Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) +- Return 40x error codes if branch could not be deleted in UI (Stan Hu) +- Remove project visibility icons from dashboard projects list +- Rename "Design" profile settings page to "Preferences". +- Allow users to customize their default Dashboard page. +- Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 +- Admin can edit and remove user identities +- Convert CRLF newlines to LF when committing using the web editor. +- API request /projects/:project_id/merge_requests?state=closed will return only closed merge requests without merged one. If you need ones that were merged - use state=merged. +- Allow Administrators to filter the user list by those with or without Two-factor Authentication enabled. +- Show a user's Two-factor Authentication status in the administration area. +- Explicit error when commit not found in the CI +- Improve performance for issue and merge request pages +- Users with guest access level can not set assignee, labels or milestones for issue and merge request +- Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels +- Better performance for pages with events list, issues list and commits list +- Faster automerge check and merge itself when source and target branches are in same repository +- Correctly show anonymous authorized applications under Profile > Applications. +- Query Optimization in MySQL. +- Allow users to be blocked and unblocked via the API +- Use native Postgres database cleaning during backup restore +- Redesign project page. Show README as default instead of activity. Move project activity to separate page +- Make left menu more hierarchical and less contextual by adding back item at top +- A fork can’t have a visibility level that is greater than the original project. +- Faster code search in repository and wiki. Fixes search page timeout for big repositories +- Allow administrators to disable 2FA for a specific user +- Add error message for SSH key linebreaks +- Store commits count in database (will populate with valid values only after first push) +- Rebuild cache after push to repository in background job +- Fix transferring of project to another group using the API. + +## 7.12.2 + +- Correctly show anonymous authorized applications under Profile > Applications. +- Faster automerge check and merge itself when source and target branches are in same repository +- Audit log for user authentication +- Allow custom label to be set for authentication providers. + +## 7.12.1 + +- Fix error when deleting a user who has projects (Stan Hu) +- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) +- Add SAML to list of social_provider (Matt Firtion) +- Fix merge requests API scope to keep compatibility in 7.12.x patch release (Dmitriy Zaporozhets) +- Fix closed merge request scope at milestone page (Dmitriy Zaporozhets) +- Revert merge request states renaming +- Fix hooks for web based events with external issue references (Daniel Gerhardt) +- Improve performance for issue and merge request pages +- Compress database dumps to reduce backup size + +## 7.12.0 + +- Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) +- Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) +- Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) +- Update oauth button logos for Twitter and Google to recommended assets +- Update browser gem to version 0.8.0 for IE11 support (Stan Hu) +- Fix timeout when rendering file with thousands of lines. +- Add "Remember me" checkbox to LDAP signin form. +- Add session expiration delay configuration through UI application settings +- Don't notify users mentioned in code blocks or blockquotes. +- Omit link to generate labels if user does not have access to create them (Stan Hu) +- Show warning when a comment will add 10 or more people to the discussion. +- Disable changing of the source branch in merge request update API (Stan Hu) +- Shorten merge request WIP text. +- Add option to disallow users from registering any application to use GitLab as an OAuth provider +- Support editing target branch of merge request (Stan Hu) +- Refactor permission checks with issues and merge requests project settings (Stan Hu) +- Fix Markdown preview not working in Edit Milestone page (Stan Hu) +- Fix Zen Mode not closing with ESC key (Stan Hu) +- Allow HipChat API version to be blank and default to v2 (Stan Hu) +- Add file attachment support in Milestone description (Stan Hu) +- Fix milestone "Browse Issues" button. +- Set milestone on new issue when creating issue from index with milestone filter active. +- Make namespace API available to all users (Stan Hu) +- Add webhook support for note events (Stan Hu) +- Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) +- Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) +- Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) +- Fix git blame syntax highlighting when different commits break up lines (Stan Hu) +- Add "Resend confirmation e-mail" link in profile settings (Stan Hu) +- Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) +- Disabled expansion of top/bottom blobs for new file diffs +- Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) +- Fix resolving of relative links to repository files in AsciiDoc documents. (Jakub Jirutka) +- Use the user list from the target project in a merge request (Stan Hu) +- Default extention for wiki pages is now .md instead of .markdown (Jeroen van Baarsen) +- Add validation to wiki page creation (only [a-zA-Z0-9/_-] are allowed) (Jeroen van Baarsen) +- Fix new/empty milestones showing 100% completion value (Jonah Bishop) +- Add a note when an Issue or Merge Request's title changes +- Consistently refer to MRs as either Merged or Closed. +- Add Merged tab to MR lists. +- Prefix EmailsOnPush email subject with `[Git]`. +- Group project contributions by both name and email. +- Clarify navigation labels for Project Settings and Group Settings. +- Move user avatar and logout button to sidebar +- You can not remove user if he/she is an only owner of group +- User should be able to leave group. If not - show him proper message +- User has ability to leave project +- Add SAML support as an omniauth provider +- Allow to configure a URL to show after sign out +- Add an option to automatically sign-in with an Omniauth provider +- GitLab CI service sends .gitlab-ci.yml in each push call +- When remove project - move repository and schedule it removal +- Improve group removing logic +- Trigger create-hooks on backup restore task +- Add option to automatically link omniauth and LDAP identities +- Allow special character in users bio. I.e.: I <3 GitLab + +## 7.11.4 + +- Fix missing bullets when creating lists +- Set rel="nofollow" on external links + +## 7.11.3 + +- no changes +- Fix upgrader script (Martins Polakovs) + +## 7.11.2 + +- no changes + +## 7.11.1 + +- no changes + +## 7.11.0 + +- Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) +- Get editing comments to work in Chrome 43 again. +- Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) +- Don't show duplicate deploy keys +- Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) +- Make the first branch pushed to an empty repository the default HEAD (Stan Hu) +- Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu) +- Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) +- Add application setting to restrict user signups to e-mail domains (Stan Hu) +- Don't allow a merge request to be merged when its title starts with "WIP". +- Add a page title to every page. +- Allow primary email to be set to an email that you've already added. +- Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) +- Ignore invalid lines in .gitmodules +- Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) +- Redirect to sign in page after signing out. +- Fix "Hello @username." references not working by no longer allowing usernames to end in period. +- Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu) +- Improve project page UI +- Fix broken file browsing with relative submodule in personal projects (Stan Hu) +- Add "Reply quoting selected text" shortcut key (`r`) +- Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention. +- Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention. +- When use change branches link at MR form - save source branch selection instead of target one +- Improve handling of large diffs +- Added GitLab Event header for project hooks +- Add Two-factor authentication (2FA) for GitLab logins +- Show Atom feed buttons everywhere where applicable. +- Add project activity atom feed. +- Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits. +- Explain how to get a new password reset token in welcome emails +- Include commit comments in MR from a forked project. +- Group milestones by title in the dashboard and all other issue views. +- Query issues, merge requests and milestones with their IID through API (Julien Bianchi) +- Add default project and snippet visibility settings to the admin web UI. +- Show incompatible projects in Google Code import status (Stan Hu) +- Fix bug where commit data would not appear in some subdirectories (Stan Hu) +- Task lists are now usable in comments, and will show up in Markdown previews. +- Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu) +- Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu) +- Protect OmniAuth request phase against CSRF. +- Don't send notifications to mentioned users that don't have access to the project in question. +- Add search issues/MR by number +- Change plots to bar graphs in commit statistics screen +- Move snippets UI to fluid layout +- Improve UI for sidebar. Increase separation between navigation and content +- Improve new project command options (Ben Bodenmiller) +- Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük) +- Prevent sending empty messages to HipChat (Chulki Lee) +- Improve UI for mobile phones on dashboard and project pages +- Add room notification and message color option for HipChat +- Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka) +- Add footnotes support to Markdown (Guillaume Delbergue) +- Add current_sign_in_at to UserFull REST api. +- Make Sidekiq MemoryKiller shutdown signal configurable +- Add "Create Merge Request" buttons to commits and branches pages and push event. +- Show user roles by comments. +- Fix automatic blocking of auto-created users from Active Directory. +- Call merge request webhook for each new commits (Arthur Gautier) +- Use SIGKILL by default in Sidekiq::MemoryKiller +- Fix mentioning of private groups. +- Add style for <kbd> element in markdown +- Spin spinner icon next to "Checking for CI status..." on MR page. +- Fix reference links in dashboard activity and ATOM feeds. +- Ensure that the first added admin performs repository imports + +## 7.10.4 + +- Fix migrations broken in 7.10.2 +- Make tags for GitLab installations running on MySQL case sensitive +- Get Gitorious importer to work again. +- Fix adding new group members from admin area +- Fix DB error when trying to tag a repository (Stan Hu) +- Fix Error 500 when searching Wiki pages (Stan Hu) +- Unescape branch names in compare commit (Stan Hu) +- Order commit comments chronologically in API. + +## 7.10.2 + +- Fix CI links on MR page + +## 7.10.0 + +- Ignore submodules that are defined in .gitmodules but are checked in as directories. +- Allow projects to be imported from Google Code. +- Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger) +- Allow users to be invited by email to join a group or project. +- Don't crash when project repository doesn't exist. +- Add config var to block auto-created LDAP users. +- Don't use HTML ellipsis in EmailsOnPush subject truncated commit message. +- Set EmailsOnPush reply-to address to committer email when enabled. +- Fix broken file browsing with a submodule that contains a relative link (Stan Hu) +- Fix persistent XSS vulnerability around profile website URLs. +- Fix project import URL regex to prevent arbitary local repos from being imported. +- Fix directory traversal vulnerability around uploads routes. +- Fix directory traversal vulnerability around help pages. +- Don't leak existence of project via search autocomplete. +- Don't leak existence of group or project via search. +- Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu) +- Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu) +- Add a rake task to check repository integrity with `git fsck` +- Add ability to configure Reply-To address in gitlab.yml (Stan Hu) +- Move current user to the top of the list in assignee/author filters (Stan Hu) +- Fix broken side-by-side diff view on merge request page (Stan Hu) +- Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) +- Allow HTML tags in Markdown input +- Fix code unfold not working on Compare commits page (Stan Hu) +- Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik) +- Fix "Import projects from" button to show the correct instructions (Stan Hu) +- Fix dots in Wiki slugs causing errors (Stan Hu) +- Make maximum attachment size configurable via Application Settings (Stan Hu) +- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) +- Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) +- Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) +- Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu) +- enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) +- Fix a link in the patch update guide +- Add a service to support external wikis (Hannes Rosenögger) +- Omit the "email patches" link and fix plain diff view for merge commits +- List new commits for newly pushed branch in activity view. +- Add sidetiq gem dependency to match EE +- Add changelog, license and contribution guide links to project tab bar. +- Improve diff UI +- Fix alignment of navbar toggle button (Cody Mize) +- Fix checkbox rendering for nested task lists +- Identical look of selectboxes in UI +- Upgrade the gitlab_git gem to version 7.1.3 +- Move "Import existing repository by URL" option to button. +- Improve error message when save profile has error. +- Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) +- Add location field to user profile +- Fix print view for markdown files and wiki pages +- Fix errors when deleting old backups +- Improve GitLab performance when working with git repositories +- Add tag message and last commit to tag hook (Kamil Trzciński) +- Restrict permissions on backup files +- Improve oauth accounts UI in profile page +- Add ability to unlink connected accounts +- Replace commits calendar with faster contribution calendar that includes issues and merge requests +- Add inifinite scroll to user page activity +- Don't include system notes in issue/MR comment count. +- Don't mark merge request as updated when merge status relative to target branch changes. +- Link note avatar to user. +- Make Git-over-SSH errors more descriptive. +- Fix EmailsOnPush. +- Refactor issue filtering +- AJAX selectbox for issue assignee and author filters +- Fix issue with missing options in issue filtering dropdown if selected one +- Prevent holding Control-Enter or Command-Enter from posting comment multiple times. +- Prevent note form from being cleared when submitting failed. +- Improve file icons rendering on tree (Sullivan Sénéchal) +- API: Add pagination to project events +- Get issue links in notification mail to work again. +- Don't show commit comment button when user is not signed in. +- Fix admin user projects lists. +- Don't leak private group existence by redirecting from namespace controller to group controller. +- Ability to skip some items from backup (database, respositories or uploads) +- Archive repositories in background worker. +- Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. +- Project labels are now available over the API under the "tag_list" field (Cristian Medina) +- Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz) +- Fix and improve help rendering (Sullivan Sénéchal) +- Fix final line in EmailsOnPush email diff being rendered as error. +- Prevent duplicate Buildkite service creation. +- Fix git over ssh errors 'fatal: protocol error: bad line length character' +- Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled +- Bust group page project list cache when namespace name or path changes. +- Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded +- Allow user to choose a public email to show on public profile +- Remove truncation from issue titles on milestone page (Jason Blanchard) +- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) +- Fix merge request comments on files with multiple commits +- Fix Resource Owner Password Authentication Flow +- Add icons to Add dropdown items. +- Allow admin to create public deploy keys that are accessible to any project. +- Warn when gitlab-shell version doesn't match requirement. +- Skip email confirmation when set by admin or via LDAP. +- Only allow users to reference groups, projects, issues, MRs, commits they have access to. + +## 7.9.4 + +- Security: Fix project import URL regex to prevent arbitary local repos from being imported +- Fixed issue where only 25 commits would load in file listings +- Fix LDAP identities after config update + +## 7.9.3 + +- Contains no changes + +## 7.9.2 + +- Contains no changes + +## 7.9.1 + +- Include missing events and fix save functionality in admin service template settings form (Stan Hu) +- Fix "Import projects from" button to show the correct instructions (Stan Hu) +- Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) +- Fix for LDAP with commas in DN +- Fix missing events and in admin Slack service template settings form (Stan Hu) +- Don't show commit comment button when user is not signed in. +- Downgrade gemnasium-gitlab-service gem + +## 7.9.0 + +- Add HipChat integration documentation (Stan Hu) +- Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) +- Fix broken email images (Hannes Rosenögger) +- Automatically config git if user forgot, where possible (Zeger-Jan van de Weg) +- Fix mass SQL statements on initial push (Hannes Rosenögger) +- Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) +- Add comment notification events to HipChat and Slack services (Stan Hu) +- Add issue and merge request events to HipChat and Slack services (Stan Hu) +- Fix merge request URL passed to Webhooks. (Stan Hu) +- Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) +- Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) +- Move labels/milestones tabs to sidebar +- Upgrade Rails gem to version 4.1.9. +- Improve error messages for file edit failures +- Improve UI for commits, issues and merge request lists +- Fix commit comments on first line of diff not rendering in Merge Request Discussion view. +- Allow admins to override restricted project visibility settings. +- Move restricted visibility settings from gitlab.yml into the web UI. +- Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) +- Save web edit in new branch +- Fix ordering of imported but unchanged projects (Marco Wessel) +- Mobile UI improvements: make aside content expandable +- Expose avatar_url in projects API +- Fix checkbox alignment on the application settings page. +- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) +- Fix mass-unassignment of issues (Robert Speicher) +- Fix hidden diff comments in merge request discussion view +- Allow user confirmation to be skipped for new users via API +- Add a service to send updates to an Irker gateway (Romain Coltel) +- Add brakeman (security scanner for Ruby on Rails) +- Slack username and channel options +- Add grouped milestones from all projects to dashboard. +- Webhook sends pusher email as well as commiter +- Add Bitbucket omniauth provider. +- Add Bitbucket importer. +- Support referencing issues to a project whose name starts with a digit +- Condense commits already in target branch when updating merge request source branch. +- Send notifications and leave system comments when bulk updating issues. +- Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) +- Move groups page from profile to dashboard +- Starred projects page at dashboard +- Blocking user does not remove him/her from project/groups but show blocked label +- Change subject of EmailsOnPush emails to include namespace, project and branch. +- Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. +- Remove confusing footer from EmailsOnPush mail body. +- Add list of changed files to EmailsOnPush emails. +- Add option to send EmailsOnPush emails from committer email if domain matches. +- Add option to disable code diffs in EmailOnPush emails. +- Wrap commit message in EmailsOnPush email. +- Send EmailsOnPush emails when deleting commits using force push. +- Fix EmailsOnPush email comparison link to include first commit. +- Fix highliht of selected lines in file +- Reject access to group/project avatar if the user doesn't have access. +- Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update) +- Add GitLab active users count to rake gitlab:check +- Starred projects page at dashboard +- Make email display name configurable +- Improve json validation in hook data +- Use Emoji One +- Updated emoji help documentation to properly reference EmojiOne. +- Fix missing GitHub organisation repositories on import page. +- Added blue theme +- Remove annoying notice messages when create/update merge request +- Allow smb:// links in Markdown text. +- Filter merge request by title or description at Merge Requests page +- Block user if he/she was blocked in Active Directory +- Fix import pages not working after first load. +- Use custom LDAP label in LDAP signin form. +- Execute hooks and services when branch or tag is created or deleted through web interface. +- Block and unblock user if he/she was blocked/unblocked in Active Directory +- Raise recommended number of unicorn workers from 2 to 3 +- Use same layout and interactivity for project members as group members. +- Prevent gitlab-shell character encoding issues by receiving its changes as raw data. +- Ability to unsubscribe/subscribe to issue or merge request +- Delete deploy key when last connection to a project is destroyed. +- Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) +- Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) +- Add canceled status for CI +- Send EmailsOnPush email when branch or tag is created or deleted. +- Faster merge request processing for large repository +- Prevent doubling AJAX request with each commit visit via Turbolink +- Prevent unnecessary doubling of js events on import pages and user calendar + +## 7.8.4 + +- Fix issue_tracker_id substitution in custom issue trackers +- Fix path and name duplication in namespaces + +## 7.8.3 + +- Bump version of gitlab_git fixing annotated tags without message + +## 7.8.2 + +- Fix service migration issue when upgrading from versions prior to 7.3 +- Fix setting of the default use project limit via admin UI +- Fix showing of already imported projects for GitLab and Gitorious importers +- Fix response of push to repository to return "Not found" if user doesn't have access +- Fix check if user is allowed to view the file attachment +- Fix import check for case sensetive namespaces +- Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. +- Properly handle autosave local storage exceptions. +- Escape wildcards when searching LDAP by username. + +## 7.8.1 + +- Fix run of custom post receive hooks +- Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3 +- Fix the warning for LDAP users about need to set password +- Fix avatars which were not shown for non logged in users +- Fix urls for the issues when relative url was enabled + +## 7.8.0 + +- Fix access control and protection against XSS for note attachments and other uploads. +- Replace highlight.js with rouge-fork rugments (Stefan Tatschner) +- Make project search case insensitive (Hannes Rosenögger) +- Include issue/mr participants in list of recipients for reassign/close/reopen emails +- Expose description in groups API +- Better UI for project services page +- Cleaner UI for web editor +- Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger) +- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen) +- View note image attachments in new tab when clicked instead of downloading them +- Improve sorting logic in UI and API. Explicitly define what sorting method is used by default +- Fix overflow at sidebar when have several items +- Add notes for label changes in issue and merge requests +- Show tags in commit view (Hannes Rosenögger) +- Only count a user's vote once on a merge request or issue (Michael Clarke) +- Increase font size when browse source files and diffs +- Service Templates now let you set default values for all services +- Create new file in empty repository using GitLab UI +- Ability to clone project using oauth2 token +- Upgrade Sidekiq gem to version 3.3.0 +- Stop git zombie creation during force push check +- Show success/error messages for test setting button in services +- Added Rubocop for code style checks +- Fix commits pagination +- Async load a branch information at the commit page +- Disable blacklist validation for project names +- Allow configuring protection of the default branch upon first push (Marco Wessel) +- Add gitlab.com importer +- Add an ability to login with gitlab.com +- Add a commit calendar to the user profile (Hannes Rosenögger) +- Submit comment on command-enter +- Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. +- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger) +- Fix long broadcast message cut-off on left sidebar (Visay Keo) +- Add Project Avatars (Steven Thonus and Hannes Rosenögger) +- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. +- Edit group members via API +- Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks) +- Add action property to merge request hook (Julien Bianchi) +- Remove duplicates from group milestone participants list. +- Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) +- API: Access groups with their path (Julien Bianchi) +- Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) +- Allow notification email to be set separately from primary email. +- API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) +- Don't have Markdown preview fail for long comments/wiki pages. +- When test webhook - show error message instead of 500 error page if connection to hook url was reset +- Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) +- Added persistent collapse button for left side nav bar (Jason Blanchard) +- Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. +- Don't allow page to be scaled on mobile. +- Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. +- Show assignees in merge request index page (Kelvin Mutuma) +- Link head panel titles to relevant root page. +- Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). +- Show users button to share their newly created public or internal projects on twitter +- Add quick help links to the GitLab pricing and feature comparison pages. +- Fix duplicate authorized applications in user profile and incorrect application client count in admin area. +- Make sure Markdown previews always use the same styling as the eventual destination. +- Remove deprecated Group#owner_id from API +- Show projects user contributed to on user page. Show stars near project on user page. +- Improve database performance for GitLab +- Add Asana service (Jeremy Benoist) +- Improve project webhooks with extra data + +## 7.7.2 + +- Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch +- Fix issue when LDAP user can't login with existing GitLab account + +## 7.7.1 + +- Improve mention autocomplete performance +- Show setup instructions for GitHub import if disabled +- Allow use http for OAuth applications + +## 7.7.0 + +- Import from GitHub.com feature +- Add Jetbrains Teamcity CI service (Jason Lippert) +- Mention notification level +- Markdown preview in wiki (Yuriy Glukhov) +- Raise group avatar filesize limit to 200kb +- OAuth applications feature +- Show user SSH keys in admin area +- Developer can push to protected branches option +- Set project path instead of project name in create form +- Block Git HTTP access after 10 failed authentication attempts +- Updates to the messages returned by API (sponsored by O'Reilly Media) +- New UI layout with side navigation +- Add alert message in case of outdated browser (IE < 10) +- Added API support for sorting projects +- Update gitlab_git to version 7.0.0.rc14 +- Add API project search filter option for authorized projects +- Fix File blame not respecting branch selection +- Change some of application settings on fly in admin area UI +- Redesign signin/signup pages +- Close standard input in Gitlab::Popen.popen +- Trigger GitLab CI when push tags +- When accept merge request - do merge using sidaekiq job +- Enable web signups by default +- Fixes for diff comments: drag-n-drop images, selecting images +- Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update +- Remove password strength indicator + +## 7.6.0 + +- Fork repository to groups +- New rugged version +- Add CRON=1 backup setting for quiet backups +- Fix failing wiki restore +- Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable) +- Monokai highlighting style now more faithful to original design (Mark Riedesel) +- Create project with repository in synchrony +- Added ability to create empty repo or import existing one if project does not have repository +- Reactivate highlight.js language autodetection +- Mobile UI improvements +- Change maximum avatar file size from 100KB to 200KB +- Strict validation for snippet file names +- Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada) +- In the docker directory is a container template based on the Omnibus packages. +- Update Sidekiq to version 2.17.8 +- Add author filter to project issues and merge requests pages +- Atom feed for user activity +- Support multiple omniauth providers for the same user +- Rendering cross reference in issue title and tooltip for merge request +- Show username in comments +- Possibility to create Milestones or Labels when Issues are disabled +- Fix bug with showing gpg signature in tag + +## 7.5.3 + +- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) + +## 7.5.2 + +- Don't log Sidekiq arguments by default +- Fix restore of wiki repositories from backups + +## 7.5.1 + +- Add missing timestamps to 'members' table + +## 7.5.0 + +- API: Add support for Hipchat (Kevin Houdebert) +- Add time zone configuration in gitlab.yml (Sullivan Senechal) +- Fix LDAP authentication for Git HTTP access +- Run 'GC.start' after every EmailsOnPushWorker job +- Fix LDAP config lookup for provider 'ldap' +- Drop all sequences during Postgres database restore +- Project title links to project homepage (Ben Bodenmiller) +- Add Atlassian Bamboo CI service (Drew Blessing) +- Mentioned @user will receive email even if he is not participating in issue or commit +- Session API: Use case-insensitive authentication like in UI (Andrey Krivko) +- Tie up loose ends with annotated tags: API & UI (Sean Edge) +- Return valid json for deleting branch via API (sponsored by O'Reilly Media) +- Expose username in project events API (sponsored by O'Reilly Media) +- Adds comments to commits in the API +- Performance improvements +- Fix post-receive issue for projects with deleted forks +- New gitlab-shell version with custom hooks support +- Improve code +- GitLab CI 5.2+ support (does not support older versions) +- Fixed bug when you can not push commits starting with 000000 to protected branches +- Added a password strength indicator +- Change project name and path in one form +- Display renamed files in diff views (Vinnie Okada) +- Fix raw view for public snippets +- Use secret token with GitLab internal API. +- Add missing timestamps to 'members' table + +## 7.4.5 + +- Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) + +## 7.4.4 + +- No changes + +## 7.4.3 + +- Fix raw snippets view +- Fix security issue for member api +- Fix buildbox integration + +## 7.4.2 + +- Fix internal snippet exposing for unauthenticated users + +## 7.4.1 + +- Fix LDAP authentication for Git HTTP access +- Fix LDAP config lookup for provider 'ldap' +- Fix public snippets +- Fix 500 error on projects with nested submodules + +## 7.4.0 + +- Refactored membership logic +- Improve error reporting on users API (Julien Bianchi) +- Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally +- Default branch is protected by default +- Increase unicorn timeout to 60 seconds +- Sort search autocomplete projects by stars count so most popular go first +- Add README to tab on project show page +- Do not delete tmp/repositories itself during clean-up, only its contents +- Support for backup uploads to remote storage +- Prevent notes polling when there are not notes +- Internal ForkService: Prepare support for fork to a given namespace +- API: Add support for forking a project via the API (Bernhard Kaindl) +- API: filter project issues by milestone (Julien Bianchi) +- Fail harder in the backup script +- Changes to Slack service structure, only webhook url needed +- Zen mode for wiki and milestones (Robert Schilling) +- Move Emoji parsing to html-pipeline-gitlab (Robert Schilling) +- Font Awesome 4.2 integration (Sullivan Senechal) +- Add Pushover service integration (Sullivan Senechal) +- Add select field type for services options (Sullivan Senechal) +- Add cross-project references to the Markdown parser (Vinnie Okada) +- Add task lists to issue and merge request descriptions (Vinnie Okada) +- Snippets can be public, internal or private +- Improve danger zone: ask project path to confirm data-loss action +- Raise exception on forgery +- Show build coverage in Merge Requests (requires GitLab CI v5.1) +- New milestone and label links on issue edit form +- Improved repository graphs +- Improve event note display in dashboard and project activity views (Vinnie Okada) +- Add users sorting to admin area +- UI improvements +- Fix ambiguous sha problem with mentioned commit +- Fixed bug with apostrophe when at mentioning users +- Add active directory ldap option +- Developers can push to wiki repo. Protected branches does not affect wiki repo any more +- Faster rev list +- Fix branch removal + +## 7.3.2 + +- Fix creating new file via web editor +- Use gitlab-shell v2.0.1 + +## 7.3.1 + +- Fix ref parsing in Gitlab::GitAccess +- Fix error 500 when viewing diff on a file with changed permissions +- Fix adding comments to MR when source branch is master +- Fix error 500 when searching description contains relative link + +## 7.3.0 + +- Always set the 'origin' remote in satellite actions +- Write authorized_keys in tmp/ during tests +- Use sockets to connect to Redis +- Add dormant New Relic gem (can be enabled via environment variables) +- Expire Rack sessions after 1 week +- Cleaner signin/signup pages +- Improved comments UI +- Better search with filtering, pagination etc +- Added a checkbox to toggle line wrapping in diff (Yuriy Glukhov) +- Prevent project stars duplication when fork project +- Use the default Unicorn socket backlog value of 1024 +- Support Unix domain sockets for Redis +- Store session Redis keys in 'session:gitlab:' namespace +- Deprecate LDAP account takeover based on partial LDAP email / GitLab username match +- Use /bin/sh instead of Bash in bin/web, bin/background_jobs (Pavel Novitskiy) +- Keyboard shortcuts for productivity (Robert Schilling) +- API: filter issues by state (Julien Bianchi) +- API: filter issues by labels (Julien Bianchi) +- Add system hook for ssh key changes +- Add blob permalink link (Ciro Santilli) +- Create annotated tags through UI and API (Sean Edge) +- Snippets search (Charles Bushong) +- Comment new push to existing MR +- Add 'ci' to the blacklist of forbidden names +- Improve text filtering on issues page +- Comment & Close button +- Process git push --all much faster +- Don't allow edit of system notes +- Project wiki search (Ralf Seidler) +- Enabled Shibboleth authentication support (Matus Banas) +- Zen mode (fullscreen) for issues/MR/notes (Robert Schilling) +- Add ability to configure webhook timeout via gitlab.yml (Wes Gurney) +- Sort project merge requests in asc or desc order for updated_at or created_at field (sponsored by O'Reilly Media) +- Add Redis socket support to 'rake gitlab:shell:install' + +## 7.2.1 + +- Delete orphaned labels during label migration (James Brooks) +- Security: prevent XSS with stricter MIME types for raw repo files + +## 7.2.0 + +- Explore page +- Add project stars (Ciro Santilli) +- Log Sidekiq arguments +- Better labels: colors, ability to rename and remove +- Improve the way merge request collects diffs +- Improve compare page for large diffs +- Expose the full commit message via API +- Fix 500 error on repository rename +- Fix bug when MR download patch return invalid diff +- Test gitlab-shell integration +- Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported +- API for labels (Robert Schilling) +- API: ability to set an import url when creating project for specific user + +## 7.1.1 + +- Fix cpu usage issue in Firefox +- Fix redirect loop when changing password by new user +- Fix 500 error on new merge request page + +## 7.1.0 + +- Remove observers +- Improve MR discussions +- Filter by description on Issues#index page +- Fix bug with namespace select when create new project page +- Show README link after description for non-master members +- Add @all mention for comments +- Dont show reply button if user is not signed in +- Expose more information for issues with webhook +- Add a mention of the merge request into the default merge request commit message +- Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc +- Fix concurrency issue in repository download +- Dont allow repository name start with ? +- Improve email threading (Pierre de La Morinerie) +- Cleaner help page +- Group milestones +- Improved email notifications +- Contributors API (sponsored by Mobbr) +- Fix LDAP TLS authentication (Boris HUISGEN) +- Show VERSION information on project sidebar +- Improve branch removal logic when accept MR +- Fix bug where comment form is spawned inside the Reply button +- Remove Dir.chdir from Satellite#lock for thread-safety +- Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs! +- Show error message in case of timeout in satellite when create MR +- Show first 100 files for huge diff instead of hiding all +- Change default admin email from admin@local.host to admin@example.com + +## 7.0.0 + +- The CPU no longer overheats when you hold down the spacebar +- Improve edit file UI +- Add ability to upload group avatar when create +- Protected branch cannot be removed +- Developers can remove normal branches with UI +- Remove branch via API (sponsored by O'Reilly Media) +- Move protected branches page to Project settings area +- Redirect to Files view when create new branch via UI +- Drag and drop upload of image in every markdown-area (Earle Randolph Bunao and Neil Francis Calabroso) +- Refactor the markdown relative links processing +- Make it easier to implement other CI services for GitLab +- Group masters can create projects in group +- Deprecate ruby 1.9.3 support +- Only masters can rewrite/remove git tags +- Add X-Frame-Options SAMEORIGIN to Nginx config so Sidekiq admin is visible +- UI improvements +- Case-insensetive search for issues +- Update to rails 4.1 +- Improve performance of application for projects and groups with a lot of members +- Formally support Ruby 2.1 +- Include Nginx gitlab-ssl config +- Add manual language detection for highlight.js +- Added example.com/:username routing +- Show notice if your profile is public +- UI improvements for mobile devices +- Improve diff rendering performance +- Drag-n-drop for issues and merge requests between states at milestone page +- Fix '0 commits' message for huge repositories on project home page +- Prevent 500 error page when visit commit page from large repo +- Add notice about huge push over http to unicorn config +- File action in satellites uses default 30 seconds timeout instead of old 10 seconds one +- Overall performance improvements +- Skip init script check on omnibus-gitlab +- Be more selective when killing stray Sidekiqs +- Check LDAP user filter during sign-in +- Remove wall feature (no data loss - you can take it from database) +- Dont expose user emails via API unless you are admin +- Detect issues closed by Merge Request description +- Better email subject lines from email on push service (Alex Elman) +- Enable identicon for gravatar be default + +## 6.9.2 + +- Revert the commit that broke the LDAP user filter + +## 6.9.1 + +- Fix scroll to highlighted line +- Fix the pagination on load for commits page + +## 6.9.0 + +- Store Rails cache data in the Redis `cache:gitlab` namespace +- Adjust MySQL limits for existing installations +- Add db index on project_id+iid column. This prevents duplicate on iid (During migration duplicates will be removed) +- Markdown preview or diff during editing via web editor (Evgeniy Sokovikov) +- Give the Rails cache its own Redis namespace +- Add ability to set different ssh host, if different from http/https +- Fix syntax highlighting for code comments blocks +- Improve comments loading logic +- Stop refreshing comments when the tab is hidden +- Improve issue and merge request mobile UI (Drew Blessing) +- Document how to convert a backup to PostgreSQL +- Fix locale bug in backup manager +- Fix can not automerge when MR description is too long +- Fix wiki backup skip bug +- Two Step MR creation process +- Remove unwanted files from satellite working directory with git clean -fdx +- Accept merge request via API (sponsored by O'Reilly Media) +- Add more access checks during API calls +- Block SSH access for 'disabled' Active Directory users +- Labels for merge requests (Drew Blessing) +- Threaded emails by setting a Message-ID (Philip Blatter) + +## 6.8.0 + +- Ability to at mention users that are participating in issue and merge req. discussion +- Enabled GZip Compression for assets in example Nginx, make sure that Nginx is compiled with --with-http_gzip_static_module flag (this is default in Ubuntu) +- Make user search case-insensitive (Christopher Arnold) +- Remove omniauth-ldap nickname bug workaround +- Drop all tables before restoring a Postgres backup +- Make the repository downloads path configurable +- Create branches via API (sponsored by O'Reilly Media) +- Changed permission of gitlab-satellites directory not to be world accessible +- Protected branch does not allow force push +- Fix popen bug in `rake gitlab:satellites:create` +- Disable connection reaping for MySQL +- Allow oauth signup without email for twitter and github +- Fix faulty namespace names that caused 500 on user creation +- Option to disable standard login +- Clean old created archives from repository downloads directory +- Fix download link for huge MR diffs +- Expose event and mergerequest timestamps in API +- Fix emails on push service when only one commit is pushed + +## 6.7.3 + +- Fix the merge notification email not being sent (Pierre de La Morinerie) +- Drop all tables before restoring a Postgres backup +- Remove yanked modernizr gem + +## 6.7.2 + +- Fix upgrader script + +## 6.7.1 + +- Fix GitLab CI integration + +## 6.7.0 + +- Increased the example Nginx client_max_body_size from 5MB to 20MB, consider updating it manually on existing installations +- Add support for Gemnasium as a Project Service (Olivier Gonzalez) +- Add edit file button to MergeRequest diff +- Public groups (Jason Hollingsworth) +- Cleaner headers in Notification Emails (Pierre de La Morinerie) +- Blob and tree gfm links to anchors work +- Piwik Integration (Sebastian Winkler) +- Show contribution guide link for new issue form (Jeroen van Baarsen) +- Fix CI status for merge requests from fork +- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) +- New page load indicator that includes a spinner that scrolls with the page +- Converted all the help sections into markdown +- LDAP user filters +- Streamline the content of notification emails (Pierre de La Morinerie) +- Fixes a bug with group member administration (Matt DeTullio) +- Sort tag names using VersionSorter (Robert Speicher) +- Add GFM autocompletion for MergeRequests (Robert Speicher) +- Add webhook when a new tag is pushed (Jeroen van Baarsen) +- Add button for toggling inline comments in diff view +- Add retry feature for repository import +- Reuse the GitLab LDAP connection within each request +- Changed markdown new line behaviour to conform to markdown standards +- Fix global search +- Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) +- Create and Update MR calls now support the description parameter (Greg Messner) +- Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository +- Added Slack service integration (Federico Ravasio) +- Better API responses for access_levels (sponsored by O'Reilly Media) +- Requires at least 2 unicorn workers +- Requires gitlab-shell v1.9+ +- Replaced gemoji(due to closed licencing problem) with Phantom Open Emoji library(combined SIL Open Font License, MIT License and the CC 3.0 License) +- Fix `/:username.keys` response content type (Dmitry Medvinsky) + +## 6.6.5 + +- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) +- Hide mr close button for comment form if merge request was closed or inline comment +- Adds ability to reopen closed merge request + +## 6.6.4 + +- Add missing html escape for highlighted code blocks in comments, issues + +## 6.6.3 + +- Fix 500 error when edit yourself from admin area +- Hide private groups for public profiles + +## 6.6.2 + +- Fix 500 error on branch/tag create or remove via UI + +## 6.6.1 + +- Fix 500 error on files tab if submodules presents + +## 6.6.0 + +- Retrieving user ssh keys publically(github style): http://__HOST__/__USERNAME__.keys +- Permissions: Developer now can manage issue tracker (modify any issue) +- Improve Code Compare page performance +- Group avatar +- Pygments.rb replaced with highlight.js +- Improve Merge request diff store logic +- Improve render performnace for MR show page +- Fixed Assembla hardcoded project name +- Jira integration documentation +- Refactored app/services +- Remove snippet expiration +- Mobile UI improvements (Drew Blessing) +- Fix block/remove UI for admin::users#show page +- Show users' group membership on users' activity page (Robert Djurasaj) +- User pages are visible without login if user is authorized to a public project +- Markdown rendered headers have id derived from their name and link to their id +- Improve application to work faster with large groups (100+ members) +- Multiple emails per user +- Show last commit for file when view file source +- Restyle Issue#show page and MR#show page +- Ability to filter by multiple labels for Issues page +- Rails version to 4.0.3 +- Fixed attachment identifier displaying underneath note text (Jason Blanchard) + +## 6.5.1 + +- Fix branch selectbox when create merge request from fork + +## 6.5.0 + +- Dropdown menus on issue#show page for assignee and milestone (Jason Blanchard) +- Add color custimization and previewing to broadcast messages +- Fixed notes anchors +- Load new comments in issues dynamically +- Added sort options to Public page +- New filters (assigned/authored/all) for Dashboard#issues/merge_requests (sponsored by Say Media) +- Add project visibility icons to dashboard +- Enable secure cookies if https used +- Protect users/confirmation with rack_attack +- Default HTTP headers to protect against MIME-sniffing, force https if enabled +- Bootstrap 3 with responsive UI +- New repository download formats: tar.bz2, zip, tar (Jason Hollingsworth) +- Restyled accept widgets for MR +- SCSS refactored +- Use jquery timeago plugin +- Fix 500 error for rdoc files +- Ability to customize merge commit message (sponsored by Say Media) +- Search autocomplete via ajax +- Add website url to user profile +- Files API supports base64 encoded content (sponsored by O'Reilly Media) +- Added support for Go's repository retrieval (Bruno Albuquerque) + +## 6.4.3 + +- Don't use unicorn worker killer if PhusionPassenger is defined + +## 6.4.2 + +- Fixed wrong behaviour of script/upgrade.rb + +## 6.4.1 + +- Fixed bug with repository rename +- Fixed bug with project transfer + +## 6.4.0 + +- Added sorting to project issues page (Jason Blanchard) +- Assembla integration (Carlos Paramio) +- Fixed another 500 error with submodules +- UI: More compact issues page +- Minimal password length increased to 8 symbols +- Side-by-side diff view (Steven Thonus) +- Internal projects (Jason Hollingsworth) +- Allow removal of avatar (Drew Blessing) +- Project webhooks now support issues and merge request events +- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth) +- Expire event cache on avatar creation/removal (Drew Blessing) +- Archiving old projects (Steven Thonus) +- Rails 4 +- Add time ago tooltips to show actual date/time +- UI: Fixed UI for admin system hooks +- Ruby script for easier GitLab upgrade +- Do not remove Merge requests if fork project was removed +- Improve sign-in/signup UX +- Add resend confirmation link to sign-in page +- Set noreply@HOSTNAME for reply_to field in all emails +- Show GitLab API version on Admin#dashboard +- API Cross-origin resource sharing +- Show READMe link at project home page +- Show repo size for projects in Admin area + +## 6.3.0 + +- API for adding gitlab-ci service +- Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey) +- Restyle project home page +- Grammar fixes +- Show branches list (which branches contains commit) on commit page (Andrew Kumanyaev) +- Security improvements +- Added support for GitLab CI 4.0 +- Fixed issue with 500 error when group did not exist +- Ability to leave project +- You can create file in repo using UI +- You can remove file from repo using UI +- API: dropped default_branch attribute from project during creation +- Project default_branch is not stored in db any more. It takes from repo now. +- Admin broadcast messages +- UI improvements +- Dont show last push widget if user removed this branch +- Fix 500 error for repos with newline in file name +- Extended html titles +- API: create/update/delete repo files +- Admin can transfer project to any namespace +- API: projects/all for admin users +- Fix recent branches order + +## 6.2.4 + +- Security: Cast API private_token to string (CVE-2013-4580) +- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583) +- Fix for Git SSH access for LDAP users + +## 6.2.3 + +- Security: More protection against CVE-2013-4489 +- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546) +- Fix sidekiq rake tasks + +## 6.2.2 + +- Security: Update gitlab_git (CVE-2013-4489) + +## 6.2.1 + +- Security: Fix issue with generated passwords for new users + +## 6.2.0 + +- Public project pages are now visible to everyone (files, issues, wik, etc.) + THIS MEANS YOUR ISSUES AND WIKI FOR PUBLIC PROJECTS ARE PUBLICLY VISIBLE AFTER THE UPGRADE +- Add group access to permissions page +- Require current password to change one +- Group owner or admin can remove other group owners +- Remove group transfer since we have multiple owners +- Respect authorization in Repository API +- Improve UI for Project#files page +- Add more security specs +- Added search for projects by name to api (Izaak Alpert) +- Make default user theme configurable (Izaak Alpert) +- Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev) +- Rake tasks for webhooks management (Jonhnny Weslley) +- Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov) +- API: Remove group +- API: Remove project +- Avatar upload on profile page with a maximum of 100KB (Steven Thonus) +- Store the sessions in Redis instead of the cookie store +- Fixed relative links in markdown +- User must confirm their email if signup enabled +- User must confirm changed email + +## 6.1.0 + +- Project specific IDs for issues, mr, milestones + Above items will get a new id and for example all bookmarked issue urls will change. + Old issue urls are redirected to the new one if the issue id is too high for an internal id. +- Description field added to Merge Request +- API: Sudo api calls (Izaak Alpert) +- API: Group membership api (Izaak Alpert) +- Improved commit diff +- Improved large commit handling (Boyan Tabakov) +- Rewrite: Init script now less prone to errors and keeps better track of the service (Rovanion Luckey) +- Link issues, merge requests, and commits when they reference each other with GFM (Ash Wilson) +- Close issues automatically when pushing commits with a special message +- Improve user removal from admin area +- Invalidate events cache when project was moved +- Remove deprecated classes and rake tasks +- Add event filter for group and project show pages +- Add links to create branch/tag from project home page +- Add public-project? checkbox to new-project view +- Improved compare page. Added link to proceed into Merge Request +- Send an email to a user when they are added to group +- New landing page when you have 0 projects + +## 6.0.0 + +- Feature: Replace teams with group membership + We introduce group membership in 6.0 as a replacement for teams. + The old combination of groups and teams was confusing for a lot of people. + And when the members of a team where changed this wasn't reflected in the project permissions. + In GitLab 6.0 you will be able to add members to a group with a permission level for each member. + These group members will have access to the projects in that group. + Any changes to group members will immediately be reflected in the project permissions. + You can even have multiple owners for a group, greatly simplifying administration. +- Feature: Ability to have multiple owners for group +- Feature: Merge Requests between fork and project (Izaak Alpert) +- Feature: Generate fingerprint for ssh keys +- Feature: Ability to create and remove branches with UI +- Feature: Ability to create and remove git tags with UI +- Feature: Groups page in profile. You can leave group there +- API: Allow login with LDAP credentials +- Redesign: project settings navigation +- Redesign: snippets area +- Redesign: ssh keys page +- Redesign: buttons, blocks and other ui elements +- Add comment title to rss feed +- You can use arrows to navigate at tree view +- Add project filter on dashboard +- Cache project graph +- Drop support of root namespaces +- Default theme is classic now +- Cache result of methods like authorize_projects, project.team.members etc +- Remove $.ready events +- Fix onclick events being double binded +- Add notification level to group membership +- Move all project controllers/views under Projects:: module +- Move all profile controllers/views under Profiles:: module +- Apply user project limit only for personal projects +- Unicorn is default web server again +- Store satellites lock files inside satellites dir +- Disabled threadsafety mode in rails +- Fixed bug with loosing MR comments +- Improved MR comments logic +- Render readme file for projects in public area + +## 5.4.2 + +- Security: Cast API private_token to string (CVE-2013-4580) +- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583) + +## 5.4.1 + +- Security: Fixes for CVE-2013-4489 +- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546) + +## 5.4.0 + +- Ability to edit own comments +- Documentation improvements +- Improve dashboard projects page +- Fixed nav for empty repos +- GitLab Markdown help page +- Misspelling fixes +- Added support of unicorn and fog gems +- Added client list to API doc +- Fix PostgreSQL database restoration problem +- Increase snippet content column size +- allow project import via git:// url +- Show participants on issues, including mentions +- Notify mentioned users with email + +## 5.3.0 + +- Refactored services +- Campfire service added +- HipChat service added +- Fixed bug with LDAP + git over http +- Fixed bug with google analytics code being ignored +- Improve sign-in page if ldap enabled +- Respect newlines in wall messages +- Generate the Rails secret token on first run +- Rename repo feature +- Init.d: remove gitlab.socket on service start +- Api: added teams api +- Api: Prevent blob content being escaped +- Api: Smart deploy key add behaviour +- Api: projects/owned.json return user owned project +- Fix bug with team assignation on project from #4109 +- Advanced snippets: public/private, project/personal (Andrew Kulakov) +- Repository Graphs (Karlo Nicholas T. Soriano) +- Fix dashboard lost if comment on commit +- Update gitlab-grack. Fixes issue with --depth option +- Fix project events duplicate on project page +- Fix postgres error when displaying network graph. +- Fix dashboard event filter when navigate via turbolinks +- init.d: Ensure socket is removed before starting service +- Admin area: Style teams:index, group:show pages +- Own page for failed forking +- Scrum view for milestone + +## 5.2.0 + +- Turbolinks +- Git over http with ldap credentials +- Diff with better colors and some spacing on the corners +- Default values for project features +- Fixed huge_commit view +- Restyle project clone panel +- Move Gitlab::Git code to gitlab_git gem +- Move update docs in repo +- Requires gitlab-shell v1.4.0 +- Fixed submodules listing under file tab +- Fork feature (Angus MacArthur) +- git version check in gitlab:check +- Shared deploy keys feature +- Ability to generate default labels set for issues +- Improve gfm autocomplete (Harold Luo) +- Added support for Google Analytics +- Code search feature (Javier Castro) + +## 5.1.0 + +- You can login with email or username now +- Corrected project transfer rollback when repository cannot be moved +- Move both repo and wiki when project transfer requested +- Admin area: project editing was removed from admin namespace +- Access: admin user has now access to any project. +- Notification settings +- Gitlab::Git set of objects to abstract from grit library +- Replace Unicorn web server with Puma +- Backup/Restore refactored. Backup dump project wiki too now +- Restyled Issues list. Show milestone version in issue row +- Restyled Merge Request list +- Backup now dump/restore uploads +- Improved performance of dashboard (Andrew Kumanyaev) +- File history now tracks renames (Akzhan Abdulin) +- Drop wiki migration tools +- Drop sqlite migration tools +- project tagging +- Paginate users in API +- Restyled network graph (Hiroyuki Sato) + +## 5.0.1 + +- Fixed issue with gitlab-grit being overridden by grit + +## 5.0.0 + +- Replaced gitolite with gitlab-shell +- Removed gitolite-related libraries +- State machine added +- Setup gitlab as git user +- Internal API +- Show team tab for empty projects +- Import repository feature +- Updated rails +- Use lambda for scopes +- Redesign admin area -> users +- Redesign admin area -> user +- Secure link to file attachments +- Add validations for Group and Team names +- Restyle team page for project +- Update capybara, rspec-rails, poltergeist to recent versions +- Wiki on git using Gollum +- Added Solarized Dark theme for code review +- Don't show user emails in autocomplete lists, profile pages +- Added settings tab for group, team, project +- Replace user popup with icons in header +- Handle project moving with gitlab-shell +- Added select2-rails for selectboxes with ajax data load +- Fixed search field on projects page +- Added teams to search autocomplete +- Move groups and teams on dashboard sidebar to sub-tabs +- API: improved return codes and docs. (Felix Gilcher, Sebastian Ziebell) +- Redesign wall to be more like chat +- Snippets, Wall features are disabled by default for new projects + +## 4.2.0 + +- Teams +- User show page. Via /u/username +- Show help contents on pages for better navigation +- Async gitolite calls +- added satellites logs +- can_create_group, can_create_team booleans for User +- Process webhooks async +- GFM: Fix images escaped inside links +- Network graph improved +- Switchable branches for network graph +- API: Groups +- Fixed project download + +## 4.1.0 + +- Optional Sign-Up +- Discussions +- Satellites outside of tmp +- Line numbers for blame +- Project public mode +- Public area with unauthorized access +- Load dashboard events with ajax +- remember dashboard filter in cookies +- replace resque with sidekiq +- fix routing issues +- cleanup rake tasks +- fix backup/restore +- scss cleanup +- show preview for note images +- improved network-graph +- get rid of app/roles/ +- added new classes Team, Repository +- Reduce amount of gitolite calls +- Ability to add user in all group projects +- remove deprecated configs +- replaced Korolev font with open font +- restyled admin/dashboard page +- restyled admin/projects page + +## 4.0.0 + +- Remove project code and path from API. Use id instead +- Return valid cloneable url to repo for webhook +- Fixed backup issue +- Reorganized settings +- Fixed commits compare +- Refactored scss +- Improve status checks +- Validates presence of User#name +- Fixed postgres support +- Removed sqlite support +- Modified post-receive hook +- Milestones can be closed now +- Show comment events on dashboard +- Quick add team members via group#people page +- [API] expose created date for hooks and SSH keys +- [API] list, create issue notes +- [API] list, create snippet notes +- [API] list, create wall notes +- Remove project code - use path instead +- added username field to user +- rake task to fill usernames based on emails create namespaces for users +- STI Group < Namespace +- Project has namespace_id +- Projects with namespaces also namespaced in gitolite and stored in subdir +- Moving project to group will move it under group namespace +- Ability to move project from namespaces to another +- Fixes commit patches getting escaped (see #2036) +- Support diff and patch generation for commits and merge request +- MergeReqest doesn't generate a temporary file for the patch any more +- Update the UI to allow downloading Patch or Diff + +## 3.1.0 + +- Updated gems +- Services: Gitlab CI integration +- Events filter on dashboard +- Own namespace for redis/resque +- Optimized commit diff views +- add alphabetical order for projects admin page +- Improved web editor +- Commit stats page +- Documentation split and cleanup +- Link to commit authors everywhere +- Restyled milestones list +- added Milestone to Merge Request +- Restyled Top panel +- Refactored Satellite Code +- Added file line links +- moved from capybara-webkit to poltergeist + phantomjs + +## 3.0.3 + +- Fixed bug with issues list in Chrome +- New Feature: Import team from another project + +## 3.0.2 + +- Fixed gitlab:app:setup +- Fixed application error on empty project in admin area +- Restyled last push widget + +## 3.0.1 + +- Fixed git over http + +## 3.0.0 + +- Projects groups +- Web Editor +- Fixed bug with gitolite keys +- UI improved +- Increased performance of application +- Show user avatar in last commit when browsing Files +- Refactored Gitlab::Merge +- Use Font Awesome for icons +- Separate observing of Note and MergeRequests +- Milestone "All Issues" filter +- Fix issue close and reopen button text and styles +- Fix forward/back while browsing Tree hierarchy +- Show number of notes for commits and merge requests +- Added support pg from box and update installation doc +- Reject ssh keys that break gitolite +- [API] list one project hook +- [API] edit project hook +- [API] list project snippets +- [API] allow to authorize using private token in HTTP header +- [API] add user creation + +## 2.9.1 + +- Fixed resque custom config init + +## 2.9.0 + +- fixed inline notes bugs +- refactored rspecs +- refactored gitolite backend +- added factory_girl +- restyled projects list on dashboard +- ssh keys validation to prevent gitolite crash +- send notifications if changed permission in project +- scss refactoring. gitlab_bootstrap/ dir +- fix git push http body bigger than 112k problem +- list of labels page under issues tab +- API for milestones, keys +- restyled buttons +- OAuth +- Comment order changed + +## 2.8.1 + +- ability to disable gravatars +- improved MR diff logic +- ssh key help page + +## 2.8.0 + +- Gitlab Flavored Markdown +- Bulk issues update +- Issues API +- Cucumber coverage increased +- Post-receive files fixed +- UI improved +- Application cleanup +- more cucumber +- capybara-webkit + headless + +## 2.7.0 + +- Issue Labels +- Inline diff +- Git HTTP +- API +- UI improved +- System hooks +- UI improved +- Dashboard events endless scroll +- Source performance increased + +## 2.6.0 + +- UI polished +- Improved network graph + keyboard nav +- Handle huge commits +- Last Push widget +- Bugfix +- Better performance +- Email in resque +- Increased test coverage +- Ability to remove branch with MR accept +- a lot of code refactored + +## 2.5.0 + +- UI polished +- Git blame for file +- Bugfix +- Email in resque +- Better test coverage + +## 2.4.0 + +- Admin area stats page +- Ability to block user +- Simplified dashboard area +- Improved admin area +- Bootstrap 2.0 +- Responsive layout +- Big commits handling +- Performance improved +- Milestones + +## 2.3.1 + +- Issues pagination +- ssl fixes +- Merge Request pagination + +## 2.3.0 + +- Dashboard r1 +- Search r1 +- Project page +- Close merge request on push +- Persist MR diff after merge +- mysql support +- Documentation + +## 2.2.0 + +- We’ve added support of LDAP auth +- Improved permission logic (4 roles system) +- Protected branches (now only masters can push to protected branches) +- Usability improved +- twitter bootstrap integrated +- compare view between commits +- wiki feature +- now you can enable/disable issues, wiki, wall features per project +- security fixes +- improved code browsing (ajax branch switch etc) +- improved per-line commenting +- git submodules displayed +- moved to rails 3.2 +- help section improved + +## 2.1.0 + +- Project tab r1 +- List branches/tags +- per line comments +- mass user import + +## 2.0.0 + +- gitolite as main git host system +- merge requests +- project/repo access +- link to commit/issue feed +- design tab +- improved email notifications +- restyled dashboard +- bugfix + +## 1.2.2 + +- common config file gitlab.yml +- issues restyle +- snippets restyle +- clickable news feed header on dashboard +- bugfix + +## 1.2.1 + +- bugfix + +## 1.2.0 + +- new design +- user dashboard +- network graph +- markdown support for comments +- encoding issues +- wall like twitter timeline + +## 1.1.0 + +- project dashboard +- wall redesigned +- feature: code snippets +- fixed horizontal scroll on file preview +- fixed app crash if commit message has invalid chars +- bugfix & code cleaning + +## 1.0.2 + +- fixed bug with empty project +- added adv validation for project path & code +- feature: issues can be sortable +- bugfix +- username displayed on top panel + +## 1.0.1 + +- fixed: with invalid source code for commit +- fixed: lose branch/tag selection when use tree navigation +- when history clicked - display path +- bug fix & code cleaning + +## 1.0.0 + +- bug fix +- projects preview mode + +## 0.9.6 + +- css fix +- new repo empty tree until restart server - fixed + +## 0.9.4 + +- security improved +- authorization improved +- html escaping +- bug fix +- increased test coverage +- design improvements + +## 0.9.1 + +- increased test coverage +- design improvements +- new issue email notification +- updated app name +- issue redesigned +- issue can be edit + +## 0.8.0 + +- syntax highlight for main file types +- redesign +- stability +- security fixes +- increased test coverage +- email notification diff --git a/changelogs/unreleased/.gitkeep b/changelogs/unreleased/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/changelogs/unreleased/.gitkeep diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7a9376def02..195108b921b 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'] ||= [] # @@ -299,6 +299,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire 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' +Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *' +Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' # # GitLab Shell diff --git a/config/initializers/gitlab_workhorse_secret.rb b/config/initializers/gitlab_workhorse_secret.rb new file mode 100644 index 00000000000..ed54dc11098 --- /dev/null +++ b/config/initializers/gitlab_workhorse_secret.rb @@ -0,0 +1,8 @@ +begin + Gitlab::Workhorse.secret +rescue + Gitlab::Workhorse.write_secret +end + +# Try a second time. If it does not work this will raise. +Gitlab::Workhorse.secret diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 74fef7cadfe..5892c1de024 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -18,6 +18,7 @@ if Rails.env.production? # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) + config.tags = { program: Gitlab::Sentry.program_context } end end end diff --git a/config/routes.rb b/config/routes.rb index e93b640fbc0..068c92d1400 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -157,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 @@ -753,6 +747,7 @@ Rails.application.routes.draw do get :branch_to get :update_branches get :diff_for_path + post :bulk_update end resources :discussions, only: [], constraints: { id: /\h{40}/ } do @@ -788,6 +783,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/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/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/20160830203109_add_confidential_issues_events_to_web_hooks.rb b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb new file mode 100644 index 00000000000..a27947212f6 --- /dev/null +++ b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb @@ -0,0 +1,15 @@ +class AddConfidentialIssuesEventsToWebHooks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :web_hooks, :confidential_issues_events, :boolean, default: false, allow_null: false + end + + def down + remove_column :web_hooks, :confidential_issues_events + end +end diff --git a/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb new file mode 100644 index 00000000000..030e7c39350 --- /dev/null +++ b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb @@ -0,0 +1,15 @@ +class AddConfidentialIssuesEventsToServices < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :services, :confidential_issues_events, :boolean, default: true, allow_null: false + end + + def down + remove_column :services, :confidential_issues_events + 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/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb new file mode 100644 index 00000000000..2d76a015a08 --- /dev/null +++ b/db/migrate/20160831214002_create_project_features.rb @@ -0,0 +1,16 @@ +class CreateProjectFeatures < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :project_features do |t| + t.belongs_to :project, index: true + t.integer :merge_requests_access_level + t.integer :issues_access_level + t.integer :wiki_access_level + t.integer :snippets_access_level + t.integer :builds_access_level + + t.timestamps + end + end +end diff --git a/db/migrate/20160831214543_migrate_project_features.rb b/db/migrate/20160831214543_migrate_project_features.rb new file mode 100644 index 00000000000..93f9821bc76 --- /dev/null +++ b/db/migrate/20160831214543_migrate_project_features.rb @@ -0,0 +1,44 @@ +class MigrateProjectFeatures < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = + <<-EOT + Migrating issues_enabled, merge_requests_enabled, wiki_enabled, builds_enabled, snippets_enabled fields from projects to + a new table called project_features. + EOT + + def up + sql = + %Q{ + INSERT INTO project_features(project_id, issues_access_level, merge_requests_access_level, wiki_access_level, + builds_access_level, snippets_access_level, created_at, updated_at) + SELECT + id AS project_id, + CASE WHEN issues_enabled IS true THEN 20 ELSE 0 END AS issues_access_level, + CASE WHEN merge_requests_enabled IS true THEN 20 ELSE 0 END AS merge_requests_access_level, + CASE WHEN wiki_enabled IS true THEN 20 ELSE 0 END AS wiki_access_level, + CASE WHEN builds_enabled IS true THEN 20 ELSE 0 END AS builds_access_level, + CASE WHEN snippets_enabled IS true THEN 20 ELSE 0 END AS snippets_access_level, + created_at, + updated_at + FROM projects + } + + execute(sql) + end + + def down + sql = %Q{ + UPDATE projects + SET + issues_enabled = COALESCE((SELECT CASE WHEN issues_access_level = 20 THEN true ELSE false END AS issues_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + merge_requests_enabled = COALESCE((SELECT CASE WHEN merge_requests_access_level = 20 THEN true ELSE false END AS merge_requests_enabled FROM project_features WHERE project_features.project_id = projects.id),true), + wiki_enabled = COALESCE((SELECT CASE WHEN wiki_access_level = 20 THEN true ELSE false END AS wiki_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + builds_enabled = COALESCE((SELECT CASE WHEN builds_access_level = 20 THEN true ELSE false END AS builds_enabled FROM project_features WHERE project_features.project_id = projects.id), true), + snippets_enabled = COALESCE((SELECT CASE WHEN snippets_access_level = 20 THEN true ELSE false END AS snippets_enabled FROM project_features WHERE project_features.project_id = projects.id),true) + } + + execute(sql) + end +end diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb new file mode 100644 index 00000000000..a2c207b49ea --- /dev/null +++ b/db/migrate/20160831223750_remove_features_enabled_from_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 RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + DOWNTIME_REASON = "Removing fields from database requires downtine." + + def up + remove_column :projects, :issues_enabled + remove_column :projects, :merge_requests_enabled + remove_column :projects, :builds_enabled + remove_column :projects, :wiki_enabled + remove_column :projects, :snippets_enabled + end + + # Ugly SQL but the only way i found to make it work on both Postgres and Mysql + # It will be slow but it is ok since it is a revert method + def down + add_column_with_default(:projects, :issues_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :merge_requests_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :builds_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :wiki_enabled, :boolean, default: true, allow_null: false) + add_column_with_default(:projects, :snippets_enabled, :boolean, default: true, allow_null: false) + end +end diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb new file mode 100644 index 00000000000..f1a1f001cb3 --- /dev/null +++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb @@ -0,0 +1,15 @@ +class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + update_column_in_batches(:web_hooks, :confidential_issues_events, true) do |table, query| + query.where(table[:issues_events].eq(true)) + end + end + + def down + # noop + end +end diff --git a/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb new file mode 100644 index 00000000000..a80a57254dd --- /dev/null +++ b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb @@ -0,0 +1,39 @@ +class DropGitoriousFieldFromApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # After the deploy the caches will be cold anyway + DOWNTIME = false + + def up + require 'yaml' + + import_sources = connection.execute('SELECT import_sources FROM application_settings;') + return unless import_sources.first # support empty databases + + yaml = if Gitlab::Database.postgresql? + import_sources.values[0][0] + else + import_sources.first[0] + end + + yaml = YAML.safe_load(yaml) + yaml.delete 'gitorious' + + # No need for a WHERE clause as there is only one + connection.execute("UPDATE application_settings SET import_sources = #{update_yaml(yaml)}") + end + + def down + # noop, gitorious still yields a 404 anyway + end + + private + + def connection + ActiveRecord::Base.connection + end + + def update_yaml(yaml) + connection.quote(YAML.dump(yaml)) + end +end diff --git a/db/schema.rb b/db/schema.rb index 802c928b2fc..5c283141084 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: 20160823081327) do +ActiveRecord::Schema.define(version: 20160902122721) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -295,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160823081327) 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" @@ -360,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160823081327) 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 @@ -593,7 +576,7 @@ ActiveRecord::Schema.define(version: 20160823081327) 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 @@ -783,6 +766,19 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree + create_table "project_features", force: :cascade do |t| + t.integer "project_id" + t.integer "merge_requests_access_level" + t.integer "issues_access_level" + t.integer "wiki_access_level" + t.integer "snippets_access_level" + t.integer "builds_access_level" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree + create_table "project_group_links", force: :cascade do |t| t.integer "project_id", null: false t.integer "group_id", null: false @@ -807,11 +803,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" t.integer "visibility_level", default: 0, null: false @@ -825,7 +817,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" @@ -842,6 +833,7 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.string "repository_storage", default: "default", null: false 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 @@ -917,19 +909,20 @@ ActiveRecord::Schema.define(version: 20160823081327) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.boolean "template", default: false - t.boolean "push_events", default: true - t.boolean "issues_events", default: true - t.boolean "merge_requests_events", default: true - t.boolean "tag_push_events", default: true - t.boolean "note_events", default: true, null: false - t.boolean "build_events", default: false, null: false - t.string "category", default: "common", null: false - t.boolean "default", default: false - t.boolean "wiki_page_events", default: true - t.boolean "pipeline_events", default: false, null: false + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + t.boolean "merge_requests_events", default: true + t.boolean "tag_push_events", default: true + t.boolean "note_events", default: true, null: false + t.boolean "build_events", default: false, null: false + t.string "category", default: "common", null: false + t.boolean "default", default: false + t.boolean "wiki_page_events", default: true + t.boolean "pipeline_events", default: false, null: false + t.boolean "confidential_issues_events", default: true, null: false end add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree @@ -1129,22 +1122,23 @@ ActiveRecord::Schema.define(version: 20160823081327) do add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree create_table "web_hooks", force: :cascade do |t| - t.string "url", limit: 2000 + t.string "url", limit: 2000 t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "type", default: "ProjectHook" + t.string "type", default: "ProjectHook" t.integer "service_id" - t.boolean "push_events", default: true, null: false - t.boolean "issues_events", default: false, null: false - t.boolean "merge_requests_events", default: false, null: false - t.boolean "tag_push_events", default: false - t.boolean "note_events", default: false, null: false - t.boolean "enable_ssl_verification", default: true - t.boolean "build_events", default: false, null: false - t.boolean "wiki_page_events", default: false, null: false + t.boolean "push_events", default: true, null: false + t.boolean "issues_events", default: false, null: false + t.boolean "merge_requests_events", default: false, null: false + t.boolean "tag_push_events", default: false + t.boolean "note_events", default: false, null: false + t.boolean "enable_ssl_verification", default: true + t.boolean "build_events", default: false, null: false + t.boolean "wiki_page_events", default: false, null: false t.string "token" - t.boolean "pipeline_events", default: false, null: false + t.boolean "pipeline_events", default: false, null: false + t.boolean "confidential_issues_events", default: false, null: false end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 047035dfb09..254394eb63e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,7 +19,6 @@ - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. -- [Koding](user/project/koding.md) Learn how to use Koding, the online IDE. ## Administrator documentation diff --git a/doc/api/README.md b/doc/api/README.md index 3e79cce0120..7661e1eea02 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -11,9 +11,10 @@ following locations: - [Award Emoji](award_emoji.md) - [Branches](branches.md) - [Builds](builds.md) -- [Build triggers](build_triggers.md) +- [Build Triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Commits](commits.md) +- [Deployments](deployments.md) - [Deploy Keys](deploy_keys.md) - [Groups](groups.md) - [Group Access Requests](access_requests.md) @@ -26,6 +27,7 @@ following locations: - [Open source license templates](licenses.md) - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) +- [Notification settings](notification_settings.md) - [Pipelines](pipelines.md) - [Projects](projects.md) including setting Webhooks - [Project Access Requests](access_requests.md) @@ -40,8 +42,9 @@ following locations: - [Sidekiq metrics](sidekiq_metrics.md) - [System Hooks](system_hooks.md) - [Tags](tags.md) -- [Users](users.md) - [Todos](todos.md) +- [Users](users.md) +- [Validate CI configuration](ci/lint.md) ### Internal CI API diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md new file mode 100644 index 00000000000..c3a9207a3ae --- /dev/null +++ b/doc/api/broadcast_messages.md @@ -0,0 +1,158 @@ +# Broadcast Messages + +> **Note:** This feature was introduced in GitLab 8.12. + +The broadcast message API is only accessible to administrators. All requests by +guests will respond with `401 Unauthorized`, and all requests by normal users +will respond with `403 Forbidden`. + +## Get all broadcast messages + +``` +GET /broadcast_messages +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +``` + +Example response: + +```json +[ + { + "message":"Example broadcast message", + "starts_at":"2016-08-24T23:21:16.078Z", + "ends_at":"2016-08-26T23:21:16.080Z", + "color":"#E75E40", + "font":"#FFFFFF", + "id":1, + "active": false + } +] +``` + +## Get a specific broadcast message + +``` +GET /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Deploy in progress", + "starts_at":"2016-08-24T23:21:16.078Z", + "ends_at":"2016-08-26T23:21:16.080Z", + "color":"#cecece", + "font":"#FFFFFF", + "id":1, + "active":false +} +``` + +## Create a broadcast message + +Responds with `400 Bad request` when the `message` parameter is missing or the +`color` or `font` values are invalid, and `201 Created` when the broadcast +message was successfully created. + +``` +POST /broadcast_messages +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ---------------------------------------------------- | +| `message` | string | yes | Message to display | +| `starts_at` | datetime | no | Starting time (defaults to current time) | +| `ends_at` | datetime | no | Ending time (defaults to one hour from current time) | +| `color` | string | no | Background color hex code | +| `font` | string | no | Foreground color hex code | + +```bash +curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages +``` + +Example response: + +```json +{ + "message":"Deploy in progress", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#cecece", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` + +## Update a broadcast message + +``` +PUT /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | +| `message` | string | no | Message to display | +| `starts_at` | datetime | no | Starting time | +| `ends_at` | datetime | no | Ending time | +| `color` | string | no | Background color hex code | +| `font` | string | no | Foreground color hex code | + +```bash +curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Update message", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#000", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` + +## Delete a broadcast message + +``` +DELETE /broadcast_messages/:id +``` + +| Attribute | Type | Required | Description | +| ----------- | -------- | -------- | ------------------------- | +| `id` | integer | yes | Broadcast message ID | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 +``` + +Example response: + +```json +{ + "message":"Update message", + "starts_at":"2016-08-26T00:41:35.060Z", + "ends_at":"2016-08-26T01:41:35.060Z", + "color":"#000", + "font":"#FFFFFF", + "id":1, + "active": true +} +``` diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md new file mode 100644 index 00000000000..0c96b3ee335 --- /dev/null +++ b/doc/api/ci/lint.md @@ -0,0 +1,49 @@ +# Validate the .gitlab-ci.yml + +> [Introduced][ce-5953] in GitLab 8.12. + +Checks if your .gitlab-ci.yml file is valid. + +``` +POST ci/lint +``` + +| Attribute | Type | Required | Description | +| ---------- | ------- | -------- | -------- | +| `content` | string | yes | the .gitlab-ci.yaml content| + +```bash +curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' +``` + +Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces. + +Example responses: + +* Valid content: + + ```json + { + "status": "valid", + "errors": [] + } + ``` + +* Invalid content: + + ```json + { + "status": "invalid", + "errors": [ + "variables config should be a hash of key value pairs" + ] + } + ``` + +* Without the content attribute: + + ```json + { + "error": "content is missing" + } + ``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 5c98c5d7565..682151d4b1d 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/string | 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/string | 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/string | 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/string | 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/string | 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/string | 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/string | 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 b194799ccbf..eed0d2fce51 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -80,7 +80,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/6" + "web_url": "http://example.com/example/example/issues/6", + "confidential": false } ] ``` @@ -158,7 +159,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": null, - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ] ``` @@ -238,7 +240,8 @@ Example response: "subscribed" : false, "user_notes_count": 1, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ] ``` @@ -303,7 +306,8 @@ Example response: "subscribed": false, "user_notes_count": 1, "due_date": null, - "web_url": "http://example.com/example/example/issues/1" + "web_url": "http://example.com/example/example/issues/1", + "confidential": false } ``` @@ -324,6 +328,7 @@ POST /projects/:id/issues | `id` | integer | yes | The ID of a project | | `title` | string | yes | The title of an issue | | `description` | string | no | The description of an issue | +| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `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 | @@ -362,7 +367,8 @@ Example response: "subscribed" : true, "user_notes_count": 0, "due_date": null, - "web_url": "http://example.com/example/example/issues/14" + "web_url": "http://example.com/example/example/issues/14", + "confidential": false } ``` @@ -385,6 +391,7 @@ PUT /projects/:id/issues/:issue_id | `issue_id` | integer | yes | The ID of a project's issue | | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | +| `confidential` | boolean | no | Updates an issue to be confidential | | `assignee_id` | integer | no | The ID of a user to assign the issue to | | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | @@ -424,7 +431,8 @@ Example response: "subscribed" : true, "user_notes_count": 0, "due_date": "2016-07-22", - "web_url": "http://example.com/example/example/issues/15" + "web_url": "http://example.com/example/example/issues/15", + "confidential": false } ``` @@ -503,7 +511,8 @@ Example response: "web_url": "https://gitlab.example.com/u/solon.cremin" }, "due_date": null, - "web_url": "http://example.com/example/example/issues/11" + "web_url": "http://example.com/example/example/issues/11", + "confidential": false } ``` @@ -559,7 +568,8 @@ Example response: "web_url": "https://gitlab.example.com/u/solon.cremin" }, "due_date": null, - "web_url": "http://example.com/example/example/issues/11" + "web_url": "http://example.com/example/example/issues/11", + "confidential": false } ``` @@ -616,7 +626,8 @@ Example response: }, "subscribed": false, "due_date": null, - "web_url": "http://example.com/example/example/issues/12" + "web_url": "http://example.com/example/example/issues/12", + "confidential": false } ``` @@ -704,7 +715,8 @@ Example response: "upvotes": 0, "downvotes": 0, "due_date": null, - "web_url": "http://example.com/example/example/issues/110" + "web_url": "http://example.com/example/example/issues/110", + "confidential": false }, "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 fd6d728dad2..6535e9a7801 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -110,8 +110,8 @@ POST /projects/:id/members | `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 -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30 +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members ``` Example response: diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f275762da3e..494040a1ce8 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -68,6 +68,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : false, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -135,6 +137,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": "9999999999999999999999999999999999999999", "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -238,6 +242,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -322,6 +328,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 0, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -397,6 +405,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -499,6 +509,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": "9999999999999999999999999999999999999999", "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -569,6 +581,8 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -724,7 +738,9 @@ Example response: }, "merge_when_build_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": true + "subscribed": true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null } ``` @@ -798,7 +814,9 @@ Example response: }, "merge_when_build_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": false + "subscribed": false, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null } ``` @@ -891,6 +909,8 @@ Example response: "merge_when_build_succeeds": false, "merge_status": "unchecked", "subscribed": true, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, "user_notes_count": 7, "should_remove_source_branch": true, "force_remove_source_branch": false, @@ -902,3 +922,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/notification_settings.md b/doc/api/notification_settings.md new file mode 100644 index 00000000000..ff6c9e4931c --- /dev/null +++ b/doc/api/notification_settings.md @@ -0,0 +1,169 @@ +# Notification settings + +>**Note:** This feature was [introduced][ce-5632] in GitLab 8.12. + +**Valid notification levels** + +The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized: + +``` +disabled +participating +watch +global +mention +custom +``` + +If the `custom` level is used, specific email events can be controlled. Notification email events are defined in the `NotificationSetting::EMAIL_EVENTS` model variable. Currently, these events are recognized: + +``` +new_note +new_issue +reopen_issue +close_issue +reassign_issue +new_merge_request +reopen_merge_request +close_merge_request +reassign_merge_request +merge_merge_request +``` + +## Global notification settings + +Get current notification settings and email address. + +``` +GET /notification_settings +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings +``` + +Example response: + +```json +{ + "level": "participating", + "notification_email": "admin@example.com" +} +``` + +## Update global notification settings + +Update current notification settings and email address. + +``` +PUT /notification_settings +``` + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/notification_settings?level=watch +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `level` | string | no | The global notification level | +| `notification_email` | string | no | The email address to send notifications | +| `new_note` | boolean | no | Enable/disable this notification | +| `new_issue` | boolean | no | Enable/disable this notification | +| `reopen_issue` | boolean | no | Enable/disable this notification | +| `close_issue` | boolean | no | Enable/disable this notification | +| `reassign_issue` | boolean | no | Enable/disable this notification | +| `new_merge_request` | boolean | no | Enable/disable this notification | +| `reopen_merge_request` | boolean | no | Enable/disable this notification | +| `close_merge_request` | boolean | no | Enable/disable this notification | +| `reassign_merge_request` | boolean | no | Enable/disable this notification | +| `merge_merge_request` | boolean | no | Enable/disable this notification | + +Example response: + +```json +{ + "level": "watch", + "notification_email": "admin@example.com" +} +``` + +## Group / project level notification settings + +Get current group or project notification settings. + +``` +GET /groups/:id/notification_settings +GET /projects/:id/notification_settings +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | + +Example response: + +```json +{ + "level": "global" +} +``` + +## Update group/project level notification settings + +Update current group/project notification settings. + +``` +PUT /groups/:id/notification_settings +PUT /projects/:id/notification_settings +``` + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/5/notification_settings?level=watch +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/notification_settings?level=custom&new_note=true +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The group/project ID or path | +| `level` | string | no | The global notification level | +| `new_note` | boolean | no | Enable/disable this notification | +| `new_issue` | boolean | no | Enable/disable this notification | +| `reopen_issue` | boolean | no | Enable/disable this notification | +| `close_issue` | boolean | no | Enable/disable this notification | +| `reassign_issue` | boolean | no | Enable/disable this notification | +| `new_merge_request` | boolean | no | Enable/disable this notification | +| `reopen_merge_request` | boolean | no | Enable/disable this notification | +| `close_merge_request` | boolean | no | Enable/disable this notification | +| `reassign_merge_request` | boolean | no | Enable/disable this notification | +| `merge_merge_request` | boolean | no | Enable/disable this notification | + +Example responses: + +```json +{ + "level": "watch" +} + +{ + "level": "custom", + "events": { + "new_note": true, + "new_issue": false, + "reopen_issue": false, + "close_issue": false, + "reassign_issue": false, + "new_merge_request": false, + "reopen_merge_request": false, + "close_merge_request": false, + "reassign_merge_request": false, + "merge_merge_request": false + } +} +``` + +[ce-5632]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632 diff --git a/doc/api/projects.md b/doc/api/projects.md index 0e4806e31c5..fe3c8709d13 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -452,6 +452,7 @@ Parameters: - `import_url` (optional) - `public_builds` (optional) - `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Create project for user @@ -478,6 +479,7 @@ Parameters: - `import_url` (optional) - `public_builds` (optional) - `only_allow_merge_if_build_succeeds` (optional) +- `lfs_enabled` (optional) ### Edit project @@ -489,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 @@ -505,13 +507,14 @@ Parameters: - `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. ### Fork project -Forks a project into the user namespace of the authenticated user. +Forks a project into the user namespace of the authenticated user or the one provided. ``` POST /projects/fork/:id @@ -519,7 +522,8 @@ 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 +- `namespace` (optional) - The ID or path of the namespace that the project will be forked to ### Star a project @@ -532,7 +536,7 @@ POST /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | 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" @@ -599,7 +603,7 @@ DELETE /projects/:id/star | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | 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" @@ -670,7 +674,7 @@ POST /projects/:id/archive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | 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" @@ -757,7 +761,7 @@ POST /projects/:id/unarchive | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| `id` | integer/string | 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" @@ -839,7 +843,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 @@ -853,7 +857,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 @@ -882,7 +886,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 @@ -998,6 +1002,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. @@ -1016,56 +1022,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 @@ -1075,6 +1071,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 @@ -1114,7 +1112,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 @@ -1125,7 +1123,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/api/users.md b/doc/api/users.md index 7e848586dbd..54f7a2a2ace 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -310,8 +310,7 @@ GET /user "can_create_group": true, "can_create_project": true, "two_factor_enabled": true, - "external": false, - "private_token": "dd34asd13as" + "external": false } ``` diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index c134106bfd0..406396deaaa 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -1,17 +1,19 @@ # CI Examples +A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates]. +If your favorite programming language or framework are missing we would love your help by sending a merge request +with a `.gitlab-ci.yml`. + +Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline: + - [Testing a PHP application](php.md) - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test a Clojure application](test-clojure-application.md) - [Test a Scala application](test-scala-application.md) - [Using `dpl` as deployment tool](deployment/README.md) -- Help your favorite programming language and GitLab by sending a merge request - with a guide for that language. - -## Outside the documentation - - [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) - [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml) + +[gitlab-ci-templates][https://gitlab.com/gitlab-org/gitlab-ci-yml] diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 20cd88c8d20..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) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 7c0fb225dac..b858029d25e 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,7 +30,8 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../../ssh/README.md). +instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment +to the SSH key, or the `before_script` will prompt for a passphrase. Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png Binary files differindex 2dee8ee6107..c2cf4b1852c 100644 --- a/doc/ci/triggers/img/builds_page.png +++ b/doc/ci/triggers/img/builds_page.png diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png Binary files differindex baf3fc183d8..fa86f0fee3d 100644 --- a/doc/ci/triggers/img/trigger_single_build.png +++ b/doc/ci/triggers/img/trigger_single_build.png diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png Binary files differindex 908355c33a5..b2fcc65d304 100644 --- a/doc/ci/triggers/img/trigger_variables.png +++ b/doc/ci/triggers/img/trigger_variables.png diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png Binary files differindex 69cec5cdebf..438f285ae2d 100644 --- a/doc/ci/triggers/img/triggers_page.png +++ b/doc/ci/triggers/img/triggers_page.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 4a7c21f811d..c32831d3aaa 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -76,8 +76,8 @@ export CI_RUNNER_DESCRIPTION="my runner" export CI_RUNNER_TAGS="docker, linux" export CI_SERVER="yes" export CI_SERVER_NAME="GitLab" -export CI_SERVER_REVISION="8.9.0" -export CI_SERVER_VERSION="70606bf" +export CI_SERVER_REVISION="70606bf" +export CI_SERVER_VERSION="8.9.0" ``` ### YAML-defined variables diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e7850aa2c9d..ff4c8ddc54b 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -6,50 +6,6 @@ GitLab Runner to manage your project's builds. If you want a quick introduction to GitLab CI, follow our [quick start guide](../quick_start/README.md). ---- - -<!-- START doctoc generated TOC please keep comment here to allow auto update --> -<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [.gitlab-ci.yml](#gitlab-ci-yml) - - [image and services](#image-and-services) - - [before_script](#before_script) - - [after_script](#after_script) - - [stages](#stages) - - [types](#types) - - [variables](#variables) - - [cache](#cache) - - [cache:key](#cache-key) -- [Jobs](#jobs) - - [script](#script) - - [stage](#stage) - - [only and except](#only-and-except) - - [job variables](#job-variables) - - [tags](#tags) - - [allow_failure](#allow_failure) - - [when](#when) - - [Manual actions](#manual-actions) - - [environment](#environment) - - [artifacts](#artifacts) - - [artifacts:name](#artifacts-name) - - [artifacts:when](#artifacts-when) - - [artifacts:expire_in](#artifacts-expire_in) - - [dependencies](#dependencies) - - [before_script and after_script](#before_script-and-after_script) -- [Git Strategy](#git-strategy) -- [Shallow cloning](#shallow-cloning) -- [Hidden jobs](#hidden-jobs) -- [Special YAML features](#special-yaml-features) - - [Anchors](#anchors) -- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) -- [Skipping builds](#skipping-builds) -- [Examples](#examples) - -<!-- END doctoc generated TOC please keep comment here to allow auto update --> - ---- - ## .gitlab-ci.yml From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) @@ -183,7 +139,7 @@ Alias for [stages](#stages). Introduced in GitLab Runner v0.5.0. GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the -build environment. The variables are stored in the git repository and are meant +build environment. The variables are stored in the Git repository and are meant to store non-sensitive project configuration, for example: ```yaml @@ -198,6 +154,8 @@ thus allowing to fine tune them. Variables can be also defined on [job level](#job-variables). +[Learn more about variables.](../variables/README.md) + ### cache >**Note:** @@ -934,24 +892,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 +928,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 +936,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 +1042,14 @@ 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. + +## Triggers + +Triggers can be used to force a rebuild of a specific branch, tag or commit, +with an API call. + +[Read more in the triggers documentation.](../triggers/README.md) ## Validate the .gitlab-ci.yml diff --git a/doc/development/README.md b/doc/development/README.md index 57f37da6f80..58c00f618fa 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -18,6 +18,8 @@ ## Process - [Code review guidelines](code_review.md) for reviewing code and having code reviewed. +- [Merge request performance guidelines](merge_request_performance_guidelines.md) + for ensuring merge requests do not negatively impact GitLab performance ## Backend howtos diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 927a1872413..39b801f761d 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. @@ -155,15 +155,30 @@ Inside the document: - Every piece of documentation that comes with a new feature should declare the GitLab version that feature got introduced. Right below the heading add a - note: `> Introduced in GitLab 8.3.`. + note: + + ``` + > Introduced in GitLab 8.3. + ``` + - If possible every feature should have a link to the MR that introduced it. The above note would be then transformed to: - `> [Introduced][ce-1242] in GitLab 8.3.`, where - the [link identifier](#links) is named after the repository (CE) and the MR - number. -- If the feature is only in GitLab EE, don't forget to mention it, like: - `> Introduced in GitLab EE 8.3.`. Otherwise, leave - this mention out. + + ``` + > [Introduced][ce-1242] in GitLab 8.3. + ``` + + , where the [link identifier](#links) is named after the repository (CE) and + the MR number. + +- If the feature is only in GitLab Enterprise Edition, don't forget to mention + it, like: + + ``` + > Introduced in GitLab Enterprise Edition 8.3. + ``` + + Otherwise, leave this mention out. ## References @@ -222,18 +237,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 +445,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/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md new file mode 100644 index 00000000000..0363bf8c1d5 --- /dev/null +++ b/doc/development/merge_request_performance_guidelines.md @@ -0,0 +1,171 @@ +# Merge Request Performance Guidelines + +To ensure a merge request does not negatively impact performance of GitLab +_every_ merge request **must** adhere to the guidelines outlined in this +document. There are no exceptions to this rule unless specifically discussed +with and agreed upon by merge request endbosses and performance specialists. + +To measure the impact of a merge request you can use +[Sherlock](profiling.md#sherlock). It's also highly recommended that you read +the following guides: + +* [Performance Guidelines](performance.md) +* [What requires downtime?](what_requires_downtime.md) + +## Impact Analysis + +**Summary:** think about the impact your merge request may have on performance +and those maintaining a GitLab setup. + +Any change submitted can have an impact not only on the application itself but +also those maintaining it and those keeping it up and running (e.g. production +engineers). As a result you should think carefully about the impact of your +merge request on not only the application but also on the people keeping it up +and running. + +Can the queries used potentially take down any critical services and result in +engineers being woken up in the night? Can a malicious user abuse the code to +take down a GitLab instance? Will my changes simply make loading a certain page +slower? Will execution time grow exponentially given enough load or data in the +database? + +These are all questions one should ask themselves before submitting a merge +request. It may sometimes be difficult to assess the impact, in which case you +should ask a performance specialist to review your code. See the "Reviewing" +section below for more information. + +## Performance Review + +**Summary:** ask performance specialists to review your code if you're not sure +about the impact. + +Sometimes it's hard to assess the impact of a merge request. In this case you +should ask one of the merge request (mini) endbosses to review your changes. You +can find a list of these endbosses at <https://about.gitlab.com/team/>. An +endboss in turn can request a performance specialist to review the changes. + +## Query Counts + +**Summary:** a merge request **should not** increase the number of executed SQL +queries unless absolutely necessary. + +The number of queries executed by the code modified or added by a merge request +must not increase unless absolutely necessary. When building features it's +entirely possible you will need some extra queries, but you should try to keep +this at a minimum. + +As an example, say you introduce a feature that updates a number of database +rows with the same value. It may be very tempting (and easy) to write this using +the following pseudo code: + +```ruby +objects_to_update.each do |object| + object.some_field = some_value + object.save +end +``` + +This will end up running one query for every object to update. This code can +easily overload a database given enough rows to update or many instances of this +code running in parallel. This particular problem is known as the +["N+1 query problem"](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations). + +In this particular case the workaround is fairly easy: + +```ruby +objects_to_update.update_all(some_field: some_value) +``` + +This uses ActiveRecord's `update_all` method to update all rows in a single +query. This in turn makes it much harder for this code to overload a database. + +## Executing Queries in Loops + +**Summary:** SQL queries **must not** be executed in a loop unless absolutely +necessary. + +Executing SQL queries in a loop can result in many queries being executed +depending on the number of iterations in a loop. This may work fine for a +development environment with little data, but in a production environment this +can quickly spiral out of control. + +There are some cases where this may be needed. If this is the case this should +be clearly mentioned in the merge request description. + +## Eager Loading + +**Summary:** always eager load associations when retrieving more than one row. + +When retrieving multiple database records for which you need to use any +associations you **must** eager load these associations. For example, if you're +retrieving a list of blog posts and you want to display their authors you +**must** eager load the author associations. + +In other words, instead of this: + +```ruby +Post.all.each do |post| + puts post.author.name +end +``` + +You should use this: + +```ruby +Post.all.includes(:author).each do |post| + puts post.author.name +end +``` + +## Memory Usage + +**Summary:** merge requests **must not** increase memory usage unless absolutely +necessary. + +A merge request must not increase the memory usage of GitLab by more than the +absolute bare minimum required by the code. This means that if you have to parse +some large document (e.g. an HTML document) it's best to parse it as a stream +whenever possible, instead of loading the entire input into memory. Sometimes +this isn't possible, in that case this should be stated explicitly in the merge +request. + +## Lazy Rendering of UI Elements + +**Summary:** only render UI elements when they're actually needed. + +Certain UI elements may not always be needed. For example, when hovering over a +diff line there's a small icon displayed that can be used to create a new +comment. Instead of always rendering these kind of elements they should only be +rendered when actually needed. This ensures we don't spend time generating +Haml/HTML when it's not going to be used. + +## Instrumenting New Code + +**Summary:** always add instrumentation for new classes, modules, and methods. + +Newly added classes, modules, and methods must be instrumented. This ensures +we can track the performance of this code over time. + +For more information see [Instrumentation](instrumentation.md). This guide +describes how to add instrumentation and where to add it. + +## Use of Caching + +**Summary:** cache data in memory or in Redis when it's needed multiple times in +a transaction or has to be kept around for a certain time period. + +Sometimes certain bits of data have to be re-used in different places during a +transaction. In these cases this data should be cached in memory to remove the +need for running complex operations to fetch the data. You should use Redis if +data should be cached for a certain time period instead of the duration of the +transaction. + +For example, say you process multiple snippets of text containiner username +mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the +user objects for every username we can remove the need for running the same +query for every mention of `@alice`. + +Caching data per transaction can be done using +[RequestStore](https://github.com/steveklabnik/request_store). Caching data in +Redis can be done using [Rails' caching +system](http://guides.rubyonrails.org/caching_with_rails.html). 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/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md index 57136ac5c39..ff10a98e8f5 100644 --- a/doc/gitlab-basics/add-file.md +++ b/doc/gitlab-basics/add-file.md @@ -25,7 +25,3 @@ Add all the information that you'd like to include in your file: Add a commit message based on what you just added and then click on "commit changes":  - -### Note -Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling -what the directory is about. It's the first document you'll find when you open a directory. diff --git a/doc/install/installation.md b/doc/install/installation.md index d4b89fa8345..df98655c396 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -268,9 +268,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-12-stable gitlab -**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -331,6 +331,9 @@ sudo usermod -aG redis git # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed sudo -u git -H git config --global gc.auto 0 + # Enable packfile bitmaps + sudo -u git -H git config --global repack.writeBitmaps true + # Configure Redis connection settings sudo -u git -H cp config/resque.yml.example config/resque.yml @@ -397,7 +400,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.11 + sudo -u git -H git checkout v0.8.1 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 571f1a38358..766a7119943 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -63,30 +63,30 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim ### Memory -You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab! +You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab! The operating system and any other running applications will also be using memory -so keep in mind that you need at least 2GB available before running GitLab. With +so keep in mind that you need at least 4GB available before running GitLab. With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage. -- 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice. -- 1GB RAM + 1GB swap supports up to 100 users but it will be very slow -- **2GB RAM** is the **recommended** memory size for all installations and supports up to 100 users -- 4GB RAM supports up to 1,000 users -- 8GB RAM supports up to 2,000 users -- 16GB RAM supports up to 4,000 users -- 32GB RAM supports up to 8,000 users -- 64GB RAM supports up to 16,000 users -- 128GB RAM supports up to 32,000 users +- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice. +- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow +- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users +- 8GB RAM supports up to 1,000 users +- 16GB RAM supports up to 2,000 users +- 32GB RAM supports up to 4,000 users +- 64GB RAM supports up to 8,000 users +- 128GB RAM supports up to 16,000 users +- 256GB RAM supports up to 32,000 users - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) -We recommend having at least 1GB of swap on your server, even if you currently have +We recommend having at least 2GB of swap on your server, even if you currently have enough available RAM. Having swap will help reduce the chance of errors occurring if your available memory changes. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. -## Gitlab Runner +## GitLab Runner We strongly advise against installing GitLab Runner on the same machine you plan to install GitLab on. Depending on how you decide to configure GitLab Runner and @@ -113,10 +113,8 @@ It's possible to increase the amount of unicorn workers and this will usually he For most instances we recommend using: CPU cores + 1 = unicorn workers. So for a machine with 2 cores, 3 unicorn workers is ideal. -For all machines that have 1GB and up we recommend a minimum of three unicorn workers. -If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping. -With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). -If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. +For all machines that have 2GB and up we recommend a minimum of three unicorn workers. +If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). diff --git a/doc/integration/README.md b/doc/integration/README.md index 70895abbcad..c2fd299db07 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,7 +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](koding.md) Configure Koding to use IDE integration +- [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 +  + +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. + +  + +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/intro/README.md b/doc/intro/README.md index 1850031eb26..71fef50ceb4 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -23,9 +23,9 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) - [Automatically close issues from merge requests](../customization/issue_closing.md) -- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md) -- [Revert any commit](../workflow/revert_changes.md) -- [Cherry-pick any commit](../workflow/cherry_pick_changes.md) +- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md) +- [Revert any commit](../user/project/merge_requests/revert_changes.md) +- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) ## Test and Deploy diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 98721763566..b058f8e2a03 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -82,7 +82,7 @@ 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 ``` diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md new file mode 100644 index 00000000000..011c2b0e969 --- /dev/null +++ b/doc/update/8.11-to-8.12.md @@ -0,0 +1,199 @@ +# From 8.11 to 8.12 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. 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 +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-12-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-12-stable-ee +``` + +### 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.4.0 +``` + +### 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 +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.8.1 +sudo -u git -H make +``` + +### 7. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +``` + +### 8. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-11-stable:config/gitlab.yml.example origin/8-12-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +sudo -u git -H git config --global repack.writeBitmaps true +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-11-stable:lib/support/nginx/gitlab-ssl origin/8-12-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-11-stable:lib/support/nginx/gitlab origin/8-12-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-12-stable/config/initializers/smtp_settings.rb.sample#L13? + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 9. Start application + + sudo service gitlab start + sudo service nginx restart + +### 10. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.11) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.10 to 8.11](8.10-to-8.11.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/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/permissions.md b/doc/user/permissions.md index 66542861781..1498cb361c8 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -104,6 +104,15 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. +## Project features + +Project features like wiki and issues can be hidden from users depending on +which visibility level you select on project settings. + +- Disabled: disabled for everyone +- Only team members: only team members will see even if your project is public or internal +- Everyone with access: everyone can see depending on your project visibility level + ## GitLab CI GitLab CI permissions rely on the role the user has in GitLab. There are four diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 9df9ed9c9da..cac926b3e28 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -31,10 +31,9 @@ Below is a table of the definitions used for GitLab's Issue Board. There are three types of lists, the ones you create based on your labels, and two default: -- **Backlog** (default): shows all issues that do not fall in one of the other - lists. Always appears on the very left. -- **Done** (default): shows all closed issues. Always appears on the very right. -- Label list: a list based on a label. It shows all issues with that label. +- **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.  diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md index e54587fab68..c56a1efe3c2 100644 --- a/doc/user/project/koding.md +++ b/doc/user/project/koding.md @@ -68,7 +68,7 @@ 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 to build the stack and import +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**. diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md new file mode 100644 index 00000000000..f79535d1542 --- /dev/null +++ b/doc/user/project/merge_requests.md @@ -0,0 +1,166 @@ +# Merge Requests + +Merge requests allow you to exchange changes you made to source code and +collaborate with other people on the same project. + +## Authorization for merge requests + +There are two main ways to have a merge request flow with GitLab: + +1. Working with [protected branches][] in a single repository +1. Working with forks of an authoritative project + +[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md) + +## Cherry-pick changes + +Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button +in a merged merge requests or a commit. + +[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md) + +## Merge when build succeeds + +When reviewing a merge request that looks ready to merge but still has one or +more CI builds running, you can set it to be merged automatically when all +builds succeed. This way, you don't have to wait for the builds to finish and +remember to merge the request manually. + +[Learn more about merging when build succeeds.](merge_requests/merge_when_build_succeeds.md) + +## Resolve discussion comments in merge requests reviews + +Keep track of the progress during a code review with resolving comments. +Resolving comments prevents you from forgetting to address feedback and lets +you hide discussions that are no longer relevant. + +[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md) + +## Resolve conflicts + +When a merge request has conflicts, GitLab may provide the option to resolve +those conflicts in the GitLab UI. + +[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md) + +## Revert changes + +GitLab implements Git's powerful feature to revert any commit with introducing +a **Revert** button in merge requests and commit details. + +[Learn more about reverting changes in the UI](merge_requests/revert_changes.md) + +## Merge requests versions + +Every time you push to a branch that is tied to a merge request, a new version +of merge request diff is created. When you visit a merge request that contains +more than one pushes, you can select and compare the versions of those merge +request diffs. + +[Read more about the merge requests versions.](merge_requests/versions.md) + +## Work In Progress merge requests + +To prevent merge requests from accidentally being accepted before they're +completely ready, GitLab blocks the "Accept" button for merge requests that +have been marked as a **Work In Progress**. + +[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md) + +## Ignore whitespace changes in Merge Request diff view + +If you click the **Hide whitespace changes** button, you can see the diff +without whitespace changes (if there are any). This is also working when on a +specific commit page. + + + +>**Tip:** +You can append `?w=1` while on the diffs page of a merge request to ignore any +whitespace changes. + +## Tips + +Here are some tips that will help you be more efficient with merge requests in +the command line. + +> **Note:** +This section might move in its own document in the future. + +### Checkout merge requests locally + +A merge request contains all the history from a repository, plus the additional +commits added to the branch associated with the merge request. Here's a few +tricks to checkout a merge request locally. + +#### Checkout 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. For example, to check out the merge request with ID 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. + +#### Checkout locally by modifying `.git/config` for a given repository + +Locate the section for your GitLab remote in the `.git/config` file. It looks +like this: + +``` +[remote "origin"] + url = https://gitlab.com/gitlab-org/gitlab-ce.git + fetch = +refs/heads/*:refs/remotes/origin/* +``` + +You can open the file with: + +``` +git config -e +``` + +Now add the following line to the above section: + +``` +fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/* +``` + +In the end, it should look like this: + +``` +[remote "origin"] + url = https://gitlab.com/gitlab-org/gitlab-ce.git + fetch = +refs/heads/*:refs/remotes/origin/* + fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/* +``` + +Now you can fetch all the merge requests: + +``` +git fetch origin + +... +From https://gitlab.com/gitlab-org/gitlab-ce.git + * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1 + * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2 +... +``` + +And to check out a particular merge request: + +``` +git checkout origin/merge-requests/1 +``` + +[protected branches]: protected_branches.md diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md new file mode 100644 index 00000000000..59b3fe7242c --- /dev/null +++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md @@ -0,0 +1,56 @@ +# Authorization for Merge requests + +There are two main ways to have a merge request flow with GitLab: + +1. Working with [protected branches] in a single repository. +1. Working with forks of an authoritative project. + +## Protected branch flow + +With the protected branch flow everybody works within the same GitLab project. + +The project maintainers get Master access and the regular developers get +Developer access. + +The maintainers mark the authoritative branches as 'Protected'. + +The developers push feature branches to the project and create merge requests +to have their feature branches reviewed and merged into one of the protected +branches. + +By default, only users with Master access can merge changes into a protected +branch. + +**Advantages** + +- Fewer projects means less clutter. +- Developers need to consider only one remote repository. + +**Disadvantages** + +- Manual setup of protected branch required for each new project + +## Forking workflow + +With the forking workflow the maintainers get Master access and the regular +developers get Reporter access to the authoritative repository, which prohibits +them from pushing any changes to it. + +Developers create forks of the authoritative project and push their feature +branches to their own forks. + +To get their changes into master they need to create a merge request across +forks. + +**Advantages** + +- In an appropriately configured GitLab group, new projects automatically get + the required access restrictions for regular developers: fewer manual steps + to configure authorization for new projects. + +**Disadvantages** + +- The project need to keep their forks up to date, which requires more advanced + Git skills (managing multiple remotes). + +[protected branches]: ../protected_branches.md diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md new file mode 100644 index 00000000000..64b94d81024 --- /dev/null +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -0,0 +1,52 @@ +# Cherry-pick changes + +> [Introduced][ce-3514] in GitLab 8.7. + +--- + +GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] +with introducing a **Cherry-pick** button in Merge Requests and commit details. + +## Cherry-picking a Merge Request + +After the Merge Request has been merged, a **Cherry-pick** button will be available +to cherry-pick the changes introduced by that Merge Request: + + + +--- + +You can cherry-pick the changes directly into the selected branch or you can opt to +create a new Merge Request with the cherry-pick changes: + + + +## Cherry-picking a Commit + +You can cherry-pick a Commit from the Commit details page: + + + +--- + +Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes +directly into the target branch or create a new Merge Request to cherry-pick the +changes: + + + +--- + +Please note that when cherry-picking merge commits, the mainline will always be the +first parent. If you want to use a different mainline then you need to do that +from the command line. + +Here is a quick example to cherry-pick a merge commit using the second parent as the +mainline: + +```bash +git cherry-pick -m 2 7a39eb0 +``` + +[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request" +[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation" diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png Binary files differindex 7fb68cc9e9b..7fb68cc9e9b 100644 --- a/doc/workflow/img/cherry_pick_changes_commit.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png Binary files differindex 5267e04562f..5267e04562f 100644 --- a/doc/workflow/img/cherry_pick_changes_commit_modal.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png Binary files differindex 975fb13e463..975fb13e463 100644 --- a/doc/workflow/img/cherry_pick_changes_mr.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png Binary files differindex 6c003bacbe3..6c003bacbe3 100644 --- a/doc/workflow/img/cherry_pick_changes_mr_modal.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png Binary files differindex 0e4a2b23c04..0e4a2b23c04 100644 --- a/doc/workflow/merge_requests/commit_compare.png +++ b/doc/user/project/merge_requests/img/commit_compare.png diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png Binary files differnew file mode 100644 index 00000000000..06ee4908edc --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_request_diff.png diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png Binary files differindex b86e6d7b3fd..b86e6d7b3fd 100644 --- a/doc/workflow/merge_when_build_succeeds/enable.png +++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png Binary files differnew file mode 100644 index 00000000000..6b9756b7418 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png Binary files differindex 18bebf5fe92..18bebf5fe92 100644 --- a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png +++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png Binary files differindex f3ea61d8147..f3ea61d8147 100644 --- a/doc/workflow/merge_when_build_succeeds/status.png +++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png Binary files differindex e7194fc3504..e7194fc3504 100644 --- a/doc/workflow/img/revert_changes_commit.png +++ b/doc/user/project/merge_requests/img/revert_changes_commit.png diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png Binary files differindex c660ec7eaec..c660ec7eaec 100644 --- a/doc/workflow/img/revert_changes_commit_modal.png +++ b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png Binary files differindex 3002f0ac1c5..3002f0ac1c5 100644 --- a/doc/workflow/img/revert_changes_mr.png +++ b/doc/user/project/merge_requests/img/revert_changes_mr.png diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png Binary files differindex c6aaeecc8a6..c6aaeecc8a6 100644 --- a/doc/workflow/img/revert_changes_mr_modal.png +++ b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png Binary files differnew file mode 100644 index 00000000000..f279ccd7ce3 --- /dev/null +++ b/doc/user/project/merge_requests/img/versions.png diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png Binary files differindex 89c458aa8d9..89c458aa8d9 100644 --- a/doc/workflow/wip_merge_requests/blocked_accept_button.png +++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png Binary files differindex 9c37354a653..9c37354a653 100644 --- a/doc/workflow/wip_merge_requests/mark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png Binary files differindex 31f7326beb0..31f7326beb0 100644 --- a/doc/workflow/wip_merge_requests/unmark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md new file mode 100644 index 00000000000..011f9cbc381 --- /dev/null +++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md @@ -0,0 +1,46 @@ +# Merge When Build Succeeds + +When reviewing a merge request that looks ready to merge but still has one or +more CI builds running, you can set it to be merged automatically when all +builds succeed. This way, you don't have to wait for the builds to finish and +remember to merge the request manually. + + + +When you hit the "Merge When Build Succeeds" button, the status of the merge +request will be updated to represent the impending merge. If you cannot wait +for the build to succeed and want to merge immediately, this option is available +in the dropdown menu on the right of the main button. + +Both team developers and the author of the merge request have the option to +cancel the automatic merge if they find a reason why it shouldn't be merged +after all. + + + +When the build succeeds, the merge request will automatically be merged. When +the build fails, the author gets a chance to retry any failed builds, or to +push new commits to fix the failure. + +When the builds are retried and succeed on the second try, the merge request +will automatically be merged after all. When the merge request is updated with +new commits, the automatic merge is automatically canceled to allow the new +changes to be reviewed. + +## Only allow merge requests to be merged if the build succeeds + +> **Note:** +You need to have builds configured to enable this feature. + +You can prevent merge requests from being merged if their build did not succeed. + +Navigate to your project's settings page, select the +**Only allow merge requests to be merged if the build succeeds** check box and +hit **Save** for the changes to take effect. + + + +From now on, every time the build fails you will not be able to merge the merge +request from the UI, until you make the build pass. + + 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/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md new file mode 100644 index 00000000000..5ead9f4177f --- /dev/null +++ b/doc/user/project/merge_requests/revert_changes.md @@ -0,0 +1,64 @@ +# Reverting changes + +> [Introduced][ce-1990] in GitLab 8.5. + +--- + +GitLab implements Git's powerful feature to [revert any commit][git-revert] +with introducing a **Revert** button in Merge Requests and commit details. + +## Reverting a Merge Request + +_**Note:** The **Revert** button will only be available for Merge Requests +created since GitLab 8.5. However, you can still revert a Merge Request +by reverting the merge commit from the list of Commits page._ + +After the Merge Request has been merged, a **Revert** button will be available +to revert the changes introduced by that Merge Request: + + + +--- + +You can revert the changes directly into the selected branch or you can opt to +create a new Merge Request with the revert changes: + + + +--- + +After the Merge Request has been reverted, the **Revert** button will not be +available anymore. + +## Reverting a Commit + +You can revert a Commit from the Commit details page: + + + +--- + +Similar to reverting a Merge Request, you can opt to revert the changes +directly into the target branch or create a new Merge Request to revert the +changes: + + + +--- + +After the Commit has been reverted, the **Revert** button will not be available +anymore. + +Please note that when reverting merge commits, the mainline will always be the +first parent. If you want to use a different mainline then you need to do that +from the command line. + +Here is a quick example to revert a merge commit using the second parent as the +mainline: + +```bash +git revert -m 2 7a39eb0 +``` + +[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request" +[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation" diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md new file mode 100644 index 00000000000..a6aa4b47835 --- /dev/null +++ b/doc/user/project/merge_requests/versions.md @@ -0,0 +1,28 @@ +# Merge requests versions + +> Will be [introduced][ce-5467] in GitLab 8.12. + +Every time you push to a branch that is tied to a merge request, a new version +of merge request diff is created. When you visit a merge request that contains +more than one pushes, you can select and compare the versions of those merge +request diffs. + +By default, the latest version of changes is shown. However, you +can select an older one from version dropdown. + + + +You can also compare the merge request version with older one to see what is +changed since then. + +Please note that comments are disabled while viewing outdated merge versions +or comparing to versions other than base. + +--- + +>**Note:** +Merge request versions are based on push not on commit. So, if you pushed 5 +commits in a single push, it will be a single option in the dropdown. If you +pushed 5 times, that will count for 5 options. + +[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467 diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md new file mode 100644 index 00000000000..546c8bdc5e5 --- /dev/null +++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md @@ -0,0 +1,17 @@ +# "Work In Progress" Merge Requests + +To prevent merge requests from accidentally being accepted before they're +completely ready, GitLab blocks the "Accept" button for merge requests that +have been marked a **Work In Progress**. + + + +To mark a merge request a Work In Progress, simply start its title with `[WIP]` +or `WIP:`. + + + +To allow a Work In Progress merge request to be accepted again when it's ready, +simply remove the `WIP` prefix. + + diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png Binary files differindex a8e635d2faf..a8e635d2faf 100644 --- a/doc/workflow/img/web_editor_new_branch_dropdown.png +++ b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png Binary files differindex 7f36b7faf63..7f36b7faf63 100644 --- a/doc/workflow/img/web_editor_new_branch_page.png +++ b/doc/user/project/repository/img/web_editor_new_branch_page.png diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png Binary files differindex d16e3c67116..d16e3c67116 100644 --- a/doc/workflow/img/web_editor_new_directory_dialog.png +++ b/doc/user/project/repository/img/web_editor_new_directory_dialog.png diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png Binary files differindex c8d77b16ee8..c8d77b16ee8 100644 --- a/doc/workflow/img/web_editor_new_directory_dropdown.png +++ b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png Binary files differindex 3fcb91c9b93..3fcb91c9b93 100644 --- a/doc/workflow/img/web_editor_new_file_dropdown.png +++ b/doc/user/project/repository/img/web_editor_new_file_dropdown.png diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png Binary files differindex 21c340b9288..21c340b9288 100644 --- a/doc/workflow/img/web_editor_new_file_editor.png +++ b/doc/user/project/repository/img/web_editor_new_file_editor.png diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png Binary files differindex c7738a4c930..c7738a4c930 100644 --- a/doc/workflow/img/web_editor_new_push_widget.png +++ b/doc/user/project/repository/img/web_editor_new_push_widget.png diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png Binary files differindex ac7415009b3..ac7415009b3 100644 --- a/doc/workflow/img/web_editor_new_tag_dropdown.png +++ b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png Binary files differindex 231e1a13fc0..231e1a13fc0 100644 --- a/doc/workflow/img/web_editor_new_tag_page.png +++ b/doc/user/project/repository/img/web_editor_new_tag_page.png diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png Binary files differindex 2755501dfd1..2755501dfd1 100644 --- a/doc/workflow/img/web_editor_start_new_merge_request.png +++ b/doc/user/project/repository/img/web_editor_start_new_merge_request.png diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png Binary files differnew file mode 100644 index 00000000000..4efc51cc423 --- /dev/null +++ b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png Binary files differnew file mode 100644 index 00000000000..67190c58823 --- /dev/null +++ b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png Binary files differnew file mode 100644 index 00000000000..47719113805 --- /dev/null +++ b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png Binary files differindex 9d6d8250bbe..9d6d8250bbe 100644 --- a/doc/workflow/img/web_editor_upload_file_dialog.png +++ b/doc/user/project/repository/img/web_editor_upload_file_dialog.png diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png Binary files differindex 6b5205b05ec..6b5205b05ec 100644 --- a/doc/workflow/img/web_editor_upload_file_dropdown.png +++ b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md new file mode 100644 index 00000000000..7c041d019bb --- /dev/null +++ b/doc/user/project/repository/web_editor.md @@ -0,0 +1,175 @@ +# GitLab Web Editor + +Sometimes it's easier to make quick changes directly from the GitLab interface +than to clone the project and use the Git command line tool. In this feature +highlight we look at how you can create a new file, directory, branch or +tag from the file browser. All of these actions are available from a single +dropdown menu. + +## Create a file + +From a project's files page, click the '+' button to the right of the branch selector. +Choose **New file** from the dropdown. + + + +--- + +Enter a file name in the **File name** box. Then, add file content in the editor +area. Add a descriptive commit message and choose a branch. The branch field +will default to the branch you were viewing in the file browser. If you enter +a new branch name, a checkbox will appear allowing you to start a new merge +request after you commit the changes. + +When you are satisfied with your new file, click **Commit Changes** at the bottom. + + + +### Template dropdowns + +When starting a new project, there are some common files which the new project +might need too. Therefore a message will be displayed by GitLab to make this +easy for you. + + + +When clicking on either `LICENSE` or `.gitignore`, a dropdown will be displayed +to provide you with a template which might be suitable for your project. + + + +The license, changelog, contribution guide, or `.gitlab-ci.yml` file could also +be added through a button on the project page. In the example below the license +has already been created, which creates a link to the license itself. + + + +>**Note:** +The **Set up CI** button will not appear on an empty repository. You have to at +least add a file in order for the button to show up. + +## Upload a file + +The ability to create a file is great when the content is text. However, this +doesn't work well for binary data such as images, PDFs or other file types. In +this case you need to upload a file. + +From a project's files page, click the '+' button to the right of the branch +selector. Choose **Upload file** from the dropdown. + + + +--- + +Once the upload dialog pops up there are two ways to upload your file. Either +drag and drop a file on the pop up or use the **click to upload** link. A file +preview will appear once you have selected a file to upload. + +Enter a commit message, choose a branch, and click **Upload file** when you are +ready. + + + +## Create a directory + +To keep files in the repository organized it is often helpful to create a new +directory. + +From a project's files page, click the '+' button to the right of the branch selector. +Choose **New directory** from the dropdown. + + + +--- + +In the new directory dialog enter a directory name, a commit message and choose +the target branch. Click **Create directory** to finish. + + + +## Create a new branch + +There are multiple ways to create a branch from GitLab's web interface. + +### Create a new branch from an issue + +> [Introduced][ce-2808] in GitLab 8.6. + +In case your development workflow dictates to have an issue for every merge +request, you can quickly create a branch right on the issue page which will be +tied with the issue itself. You can see a **New Branch** button after the issue +description, unless there is already a branch with the same name or a referenced +merge request. + + + +Once you click it, a new branch will be created that diverges from the default +branch of your project, by default `master`. The branch name will be based on +the title of the issue and as suffix it will have its ID. Thus, the example +screenshot above will yield a branch named +`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`. + +After the branch is created, you can edit files in the repository to fix +the issue. When a merge request is created based on the newly created branch, +the description field will automatically display the [issue closing pattern] +`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the +merge request is merged. + +### Create a new branch from a project's dashboard + +If you want to make changes to several files before creating a new merge +request, you can create a new branch up front. From a project's files page, +choose **New branch** from the dropdown. + + + +--- + +Enter a new **Branch name**. Optionally, change the **Create from** field +to choose which branch, tag or commit SHA this new branch will originate from. +This field will autocomplete if you start typing an existing branch or tag. +Click **Create branch** and you will be returned to the file browser on this new +branch. + + + +--- + +You can now make changes to any files, as needed. When you're ready to merge +the changes back to master you can use the widget at the top of the screen. +This widget only appears for a period of time after you create the branch or +modify files. + + + +## Create a new tag + +Tags are useful for marking major milestones such as production releases, +release candidates, and more. You can create a tag from a branch or a commit +SHA. From a project's files page, choose **New tag** from the dropdown. + + + +--- + +Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you +would like to create this new tag. You can optionally add a message and +release notes. The release notes section supports markdown format and you can +also upload an attachment. Click **Create tag** and you will be taken to the tag +list page. + + + +## Tips + +When creating or uploading a new file, or creating a new directory, you can +trigger a new merge request rather than committing directly to master. Enter +a new branch name in the **Target branch** field. You will notice a checkbox +appear that is labeled **Start a new merge request with these changes**. After +you commit the changes you will be taken to a new merge request form. + + + + +[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 +[issue closing pattern]: ../customization/issue_closing.md diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 11e1574f772..1792a0c501d 100644 --- a/doc/user/project/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 or this Friday or 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 0cf56449de2..dcb9c32ad58 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,6 +1,5 @@ # Workflow -- [Authorization for merge requests](authorization_for_merge_requests.md) - [Change your time zone](timezone.md) - [Description templates](../user/project/description_templates.md) - [Feature branch workflow](workflow.md) @@ -18,14 +17,18 @@ - [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) +- [Web Editor](../user/project/repository/web_editor.md) - [Releases](releases.md) - [Milestones](milestones.md) -- [Merge Requests](merge_requests.md) -- [Revert changes](revert_changes.md) -- [Cherry-pick changes](cherry_pick_changes.md) -- ["Work In Progress" Merge Requests](wip_merge_requests.md) -- [Merge When Build Succeeds](merge_when_build_succeeds.md) +- [Merge Requests](../user/project/merge_requests.md) + - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) + - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) + - [Merge when build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md) + - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md) + - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md) + - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) + - [Merge requests versions](../user/project/merge_requests/versions.md) + - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, BitBucket, etc](importing/README.md) - [Todos](todos.md) diff --git a/doc/workflow/authorization_for_merge_requests.md b/doc/workflow/authorization_for_merge_requests.md index d1d6d94ec11..7bf80a3ad0d 100644 --- a/doc/workflow/authorization_for_merge_requests.md +++ b/doc/workflow/authorization_for_merge_requests.md @@ -1,40 +1 @@ -# Authorization for Merge requests - -There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project. - -## Protected branch flow - -With the protected branch flow everybody works within the same GitLab project. - -The project maintainers get Master access and the regular developers get Developer access. - -The maintainers mark the authoritative branches as 'Protected'. - -The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches. - -Only users with Master access can merge changes into a protected branch. - -### Advantages - -- fewer projects means less clutter -- developers need to consider only one remote repository - -### Disadvantages - -- manual setup of protected branch required for each new project - -## Forking workflow - -With the forking workflow the maintainers get Master access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it. - -Developers create forks of the authoritative project and push their feature branches to their own forks. - -To get their changes into master they need to create a merge request across forks. - -### Advantages - -- in an appropriately configured GitLab group, new projects automatically get the required access restrictions for regular developers: fewer manual steps to configure authorization for new projects - -### Disadvantages - -- the project need to keep their forks up to date, which requires more advanced Git skills (managing multiple remotes) +This document was moved to [user/project/merge_requests/authorization_for_merge_requests](../user/project/merge_requests/authorization_for_merge_requests.md) diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md index 64b94d81024..663ffd3f746 100644 --- a/doc/workflow/cherry_pick_changes.md +++ b/doc/workflow/cherry_pick_changes.md @@ -1,52 +1 @@ -# Cherry-pick changes - -> [Introduced][ce-3514] in GitLab 8.7. - ---- - -GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] -with introducing a **Cherry-pick** button in Merge Requests and commit details. - -## Cherry-picking a Merge Request - -After the Merge Request has been merged, a **Cherry-pick** button will be available -to cherry-pick the changes introduced by that Merge Request: - - - ---- - -You can cherry-pick the changes directly into the selected branch or you can opt to -create a new Merge Request with the cherry-pick changes: - - - -## Cherry-picking a Commit - -You can cherry-pick a Commit from the Commit details page: - - - ---- - -Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes -directly into the target branch or create a new Merge Request to cherry-pick the -changes: - - - ---- - -Please note that when cherry-picking merge commits, the mainline will always be the -first parent. If you want to use a different mainline then you need to do that -from the command line. - -Here is a quick example to cherry-pick a merge commit using the second parent as the -mainline: - -```bash -git cherry-pick -m 2 7a39eb0 -``` - -[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request" -[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation" +This document was moved to [user/project/merge_requests/cherry_pick_changes](../user/project/merge_requests/cherry_pick_changes.md). diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 8119324bb62..7c0eb90d540 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -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 334a119e522..a68bb8b27ca 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -1,82 +1 @@ -# Merge Requests - -Merge requests allow you to exchange changes you made to source code - -## Only allow merge requests to be merged if the build succeeds - -You can prevent merge requests from being merged if their build did not succeed -in the project settings page. - - - -Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box. - -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: - -``` -[remote "origin"] - url = https://gitlab.com/gitlab-org/gitlab-ce.git - fetch = +refs/heads/*:refs/remotes/origin/* -``` - -Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section. - -It should look like this: - -``` -[remote "origin"] - url = https://gitlab.com/gitlab-org/gitlab-ce.git - fetch = +refs/heads/*:refs/remotes/origin/* - fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/* -``` - -Now you can fetch all the merge requests: - -``` -$ git fetch origin -From https://gitlab.com/gitlab-org/gitlab-ce.git - * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1 - * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2 -... -``` - -To check out a particular merge request: - -``` -$ git checkout origin/merge-requests/1 -``` - -## Ignore whitespace changes in Merge Request diff view - - - -If you click the "Hide whitespace changes" button, you can see the diff without whitespace changes. - - - -It is also working on commits compare view. - - +This document was moved to [user/project/merge_requests](../user/project/merge_requests.md). diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png Binary files differdeleted file mode 100644 index 3ebbfb75ea3..00000000000 --- a/doc/workflow/merge_requests/merge_request_diff.png +++ /dev/null diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png Binary files differdeleted file mode 100644 index a0db535019c..00000000000 --- a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png +++ /dev/null diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md index 75e1fdff2b2..95afd12ebdb 100644 --- a/doc/workflow/merge_when_build_succeeds.md +++ b/doc/workflow/merge_when_build_succeeds.md @@ -1,15 +1 @@ -# Merge When Build Succeeds - -When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually. - - - -When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button. - -Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all. - - - -When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure. - -When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed. +This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md). 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/revert_changes.md b/doc/workflow/revert_changes.md index 5ead9f4177f..cf1292253fc 100644 --- a/doc/workflow/revert_changes.md +++ b/doc/workflow/revert_changes.md @@ -1,64 +1 @@ -# Reverting changes - -> [Introduced][ce-1990] in GitLab 8.5. - ---- - -GitLab implements Git's powerful feature to [revert any commit][git-revert] -with introducing a **Revert** button in Merge Requests and commit details. - -## Reverting a Merge Request - -_**Note:** The **Revert** button will only be available for Merge Requests -created since GitLab 8.5. However, you can still revert a Merge Request -by reverting the merge commit from the list of Commits page._ - -After the Merge Request has been merged, a **Revert** button will be available -to revert the changes introduced by that Merge Request: - - - ---- - -You can revert the changes directly into the selected branch or you can opt to -create a new Merge Request with the revert changes: - - - ---- - -After the Merge Request has been reverted, the **Revert** button will not be -available anymore. - -## Reverting a Commit - -You can revert a Commit from the Commit details page: - - - ---- - -Similar to reverting a Merge Request, you can opt to revert the changes -directly into the target branch or create a new Merge Request to revert the -changes: - - - ---- - -After the Commit has been reverted, the **Revert** button will not be available -anymore. - -Please note that when reverting merge commits, the mainline will always be the -first parent. If you want to use a different mainline then you need to do that -from the command line. - -Here is a quick example to revert a merge commit using the second parent as the -mainline: - -```bash -git revert -m 2 7a39eb0 -``` - -[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request" -[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation" +This document was moved to [user/project/merge_requests/revert_changes](../user/project/merge_requests/revert_changes.md). diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index ee8e7862572..595c7da155b 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -1,151 +1 @@ -# GitLab Web Editor - -Sometimes it's easier to make quick changes directly from the GitLab interface -than to clone the project and use the Git command line tool. In this feature -highlight we look at how you can create a new file, directory, branch or -tag from the file browser. All of these actions are available from a single -dropdown menu. - -## Create a file - -From a project's files page, click the '+' button to the right of the branch selector. -Choose **New file** from the dropdown. - - - ---- - -Enter a file name in the **File name** box. Then, add file content in the editor -area. Add a descriptive commit message and choose a branch. The branch field -will default to the branch you were viewing in the file browser. If you enter -a new branch name, a checkbox will appear allowing you to start a new merge -request after you commit the changes. - -When you are satisfied with your new file, click **Commit Changes** at the bottom. - - - -## Upload a file - -The ability to create a file is great when the content is text. However, this -doesn't work well for binary data such as images, PDFs or other file types. In -this case you need to upload a file. - -From a project's files page, click the '+' button to the right of the branch -selector. Choose **Upload file** from the dropdown. - - - ---- - -Once the upload dialog pops up there are two ways to upload your file. Either -drag and drop a file on the pop up or use the **click to upload** link. A file -preview will appear once you have selected a file to upload. - -Enter a commit message, choose a branch, and click **Upload file** when you are -ready. - - - -## Create a directory - -To keep files in the repository organized it is often helpful to create a new -directory. - -From a project's files page, click the '+' button to the right of the branch selector. -Choose **New directory** from the dropdown. - - - ---- - -In the new directory dialog enter a directory name, a commit message and choose -the target branch. Click **Create directory** to finish. - - - -## Create a new branch - -There are multiple ways to create a branch from GitLab's web interface. - -### Create a new branch from an issue - -> [Introduced][ce-2808] in GitLab 8.6. - -In case your development workflow dictates to have an issue for every merge -request, you can quickly create a branch right on the issue page which will be -tied with the issue itself. You can see a **New Branch** button after the issue -description, unless there is already a branch with the same name or a referenced -merge request. - - - -Once you click it, a new branch will be created that diverges from the default -branch of your project, by default `master`. The branch name will be based on -the title of the issue and as suffix it will have its ID. Thus, the example -screenshot above will yield a branch named -`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`. - -After the branch is created, you can edit files in the repository to fix -the issue. When a merge request is created based on the newly created branch, -the description field will automatically display the [issue closing pattern] -`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the -merge request is merged. - -### Create a new branch from a project's dashboard - -If you want to make changes to several files before creating a new merge -request, you can create a new branch up front. From a project's files page, -choose **New branch** from the dropdown. - - - ---- - -Enter a new **Branch name**. Optionally, change the **Create from** field -to choose which branch, tag or commit SHA this new branch will originate from. -This field will autocomplete if you start typing an existing branch or tag. -Click **Create branch** and you will be returned to the file browser on this new -branch. - - - ---- - -You can now make changes to any files, as needed. When you're ready to merge -the changes back to master you can use the widget at the top of the screen. -This widget only appears for a period of time after you create the branch or -modify files. - - - -## Create a new tag - -Tags are useful for marking major milestones such as production releases, -release candidates, and more. You can create a tag from a branch or a commit -SHA. From a project's files page, choose **New tag** from the dropdown. - - - ---- - -Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you -would like to create this new tag. You can optionally add a message and -release notes. The release notes section supports markdown format and you can -also upload an attachment. Click **Create tag** and you will be taken to the tag -list page. - - - -## Tips - -When creating or uploading a new file, or creating a new directory, you can -trigger a new merge request rather than committing directly to master. Enter -a new branch name in the **Target branch** field. You will notice a checkbox -appear that is labeled **Start a new merge request with these changes**. After -you commit the changes you will be taken to a new merge request form. - - - -[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../customization/issue_closing.md +This document was moved to [user/project/repository/web_editor](../user/project/repository/web_editor.md). diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md index 46035a5e6b6..abb8002f442 100644 --- a/doc/workflow/wip_merge_requests.md +++ b/doc/workflow/wip_merge_requests.md @@ -1,13 +1 @@ -# "Work In Progress" Merge Requests - -To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**. - - - -To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`. - - - -To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix. - - +This document was moved to [user/project/merge_requests/work_in_progress_merge_requests](../user/project/merge_requests/work_in_progress_merge_requests.md). diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature index 42f5d6d2af7..0b23bbb7951 100644 --- a/features/dashboard/todos.feature +++ b/features/dashboard/todos.feature @@ -23,26 +23,6 @@ Feature: Dashboard Todos Then I should see all todos marked as done @javascript - Scenario: I filter by project - Given I filter by "Enterprise" - Then I should not see todos - - @javascript - Scenario: I filter by author - Given I filter by "John Doe" - Then I should not see todos related to "Mary Jane" in the list - - @javascript - Scenario: I filter by type - Given I filter by "Issue" - Then I should not see todos related to "Merge Requests" in the list - - @javascript - Scenario: I filter by action - Given I filter by "Mentioned" - Then I should not see todos related to "Assignments" in the list - - @javascript Scenario: I click on a todo row Given I click on the todo Then I should be directed to the corresponding page diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 967f2edb243..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 diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 03f87df7a60..11dc7f580f0 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps page.check('Issue') page.check('Merge request') page.check('Build') + page.check('Pipeline') click_on 'Save' end 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/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 60152d3da55..344b6fda9a6 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -3,7 +3,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedUser - include Select2Helper step '"John Doe" is a developer of project "Shop"' do project.team << [john_doe, :developer] @@ -54,7 +53,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.todos-pending-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content 'Done 4' - expect(page).not_to have_link project.name_with_namespace + expect(page).to have_content "You're all done!" + expect('.prepend-top-default').not_to have_link project.name_with_namespace should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}" should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" @@ -79,19 +79,31 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I filter by "Enterprise"' do - select2(enterprise.id, from: "#project_id") + click_button 'Project' + page.within '.dropdown-menu-project' do + click_link enterprise.name_with_namespace + end end step 'I filter by "John Doe"' do - select2(john_doe.id, from: "#author_id") + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link john_doe.username + end end step 'I filter by "Issue"' do - select2('Issue', from: "#type") + click_button 'Type' + page.within '.dropdown-menu-type' do + click_link 'Issue' + end end step 'I filter by "Mentioned"' do - select2("#{Todo::MENTIONED}", from: '#action_id') + click_button 'Action' + page.within '.dropdown-menu-action' do + click_link 'Mentioned' + end end step 'I should not see todos' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 9778ff4a6c7..df17b5626c6 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -31,7 +31,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "Closed"' do - click_link "Closed" + page.within('.issues-state-filters') do + click_link "Closed" + end end step 'I should see merge request "Wiki Feature"' do @@ -58,8 +60,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 +126,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/project.rb b/features/steps/project/project.rb index 76fefee9254..975c879149e 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -5,7 +5,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'change project settings' do fill_in 'project_name_edit', with: 'NewName' - uncheck 'project_issues_enabled' + select 'Disabled', from: 'project_project_feature_attributes_issues_access_level' end step 'I save project' do 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/features/steps/shared/project.rb b/features/steps/shared/project.rb index 0b4920883b8..afbd8ef1233 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -15,7 +15,7 @@ module SharedProject # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace, snippets_enabled: true) + @project ||= create(:project, name: "Shop", namespace: @user.namespace) @project.team << [@user, :master] end @@ -41,6 +41,8 @@ module SharedProject step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project') + @project.build_project_feature + @project.project_feature.save @project.team << [@user, :master] end @@ -95,7 +97,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(page).to have_content("Project name") - expect(page).to have_content("Features") + expect(page).to have_content("Feature Visibility") end def current_project diff --git a/lib/api/api.rb b/lib/api/api.rb index ecbd5a6e2fa..74ca4728695 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -31,6 +31,7 @@ module API mount ::API::AccessRequests mount ::API::AwardEmoji mount ::API::Branches + mount ::API::BroadcastMessages mount ::API::Builds mount ::API::CommitStatuses mount ::API::Commits @@ -44,11 +45,13 @@ module API mount ::API::Keys mount ::API::Labels mount ::API::LicenseTemplates + mount ::API::Lint mount ::API::Members mount ::API::MergeRequests mount ::API::Milestones mount ::API::Namespaces mount ::API::Notes + mount ::API::NotificationSettings mount ::API::Pipelines mount ::API::ProjectHooks mount ::API::ProjectSnippets @@ -67,5 +70,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/broadcast_messages.rb b/lib/api/broadcast_messages.rb new file mode 100644 index 00000000000..fb2a4148011 --- /dev/null +++ b/lib/api/broadcast_messages.rb @@ -0,0 +1,99 @@ +module API + class BroadcastMessages < Grape::API + before { authenticate! } + before { authenticated_as_admin! } + + resource :broadcast_messages do + helpers do + def find_message + BroadcastMessage.find(params[:id]) + end + end + + desc 'Get all broadcast messages' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + optional :page, type: Integer, desc: 'Current page number' + optional :per_page, type: Integer, desc: 'Number of messages per page' + end + get do + messages = BroadcastMessage.all + + present paginate(messages), with: Entities::BroadcastMessage + end + + desc 'Create a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :message, type: String, desc: 'Message to display' + optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now } + optional :ends_at, type: DateTime, desc: 'Ending time', default: -> { 1.hour.from_now } + optional :color, type: String, desc: 'Background color' + optional :font, type: String, desc: 'Foreground color' + end + post do + create_params = declared(params, include_missing: false).to_h + message = BroadcastMessage.create(create_params) + + if message.persisted? + present message, with: Entities::BroadcastMessage + else + render_validation_error!(message) + end + end + + desc 'Get a specific broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + get ':id' do + message = find_message + + present message, with: Entities::BroadcastMessage + end + + desc 'Update a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + optional :message, type: String, desc: 'Message to display' + optional :starts_at, type: DateTime, desc: 'Starting time' + optional :ends_at, type: DateTime, desc: 'Ending time' + optional :color, type: String, desc: 'Background color' + optional :font, type: String, desc: 'Foreground color' + end + put ':id' do + message = find_message + update_params = declared(params, include_missing: false).to_h + + if message.update(update_params) + present message, with: Entities::BroadcastMessage + else + render_validation_error!(message) + end + end + + desc 'Delete a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + delete ':id' do + message = find_message + + present message.destroy, with: Entities::BroadcastMessage + end + end + end +end 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 aaeb3d4800b..4f736e4ec2b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -76,15 +76,23 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled + expose :container_registry_enabled + + # Expose old field names with the new permissions methods to keep API compatible + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) } + expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) } + 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? } expose :avatar_url expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds expose :shared_with_groups do |project, options| @@ -211,6 +219,7 @@ module API expose :user_notes_count expose :upvotes, :downvotes expose :due_date + expose :confidential expose :web_url do |issue, options| Gitlab::UrlBuilder.build(issue) @@ -232,6 +241,8 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status + expose :diff_head_sha, as: :sha + expose :merge_commit_sha expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end @@ -250,6 +261,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 @@ -351,7 +375,7 @@ module API expose :access_level expose :notification_level do |member, options| if member.notification_setting - NotificationSetting.levels[member.notification_setting.level] + ::NotificationSetting.levels[member.notification_setting.level] end end end @@ -362,6 +386,21 @@ module API class GroupAccess < MemberAccess end + class NotificationSetting < Grape::Entity + expose :level + expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do + ::NotificationSetting::EMAIL_EVENTS.each do |event| + expose event + end + end + end + + class GlobalNotificationSetting < NotificationSetting + expose :notification_email do |notification_setting, options| + notification_setting.user.notification_email + end + end + class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events @@ -561,5 +600,10 @@ module API class Template < Grape::Entity expose :name, :content end + + class BroadcastMessage < Grape::Entity + expose :id, :message, :starts_at, :ends_at, :color, :font + expose :active?, as: :active + end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9d8b8d737a9..d2df77238d5 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -30,7 +30,7 @@ module API # Example Request: # POST /groups post do - authorize! :create_group, current_user + authorize! :create_group required_attributes! [:name, :path] attrs = attributes_for_keys [:name, :path, :description, :visibility_level] @@ -97,7 +97,7 @@ module API group = find_group(params[:id]) projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects - present projects, with: Entities::Project + present projects, with: Entities::Project, user: current_user end # Transfer a project to the Group namespace diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index da4b1bf9902..6a20ba95a79 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -129,7 +129,7 @@ module API forbidden! unless current_user.is_admin? end - def authorize!(action, subject) + def authorize!(action, subject = nil) forbidden! unless can?(current_user, action, subject) end @@ -148,7 +148,7 @@ module API end def can?(object, action, subject) - abilities.allowed?(object, action, subject) + Ability.allowed?(object, action, subject) end # Checks the occurrences of required attributes, each attribute must be present in the params hash @@ -408,14 +408,6 @@ module API links.join(', ') end - def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end - end - def secret_token File.read(Gitlab.config.gitlab_shell.secret_file).chomp end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 5b54c11ef62..6e6efece7c4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -105,15 +105,19 @@ module API post '/two_factor_recovery_codes' do status 200 - key = Key.find(params[:key_id]) - user = key.user + key = Key.find_by(id: params[:key_id]) + + unless key + return { 'success' => false, 'message' => 'Could not find the given key' } + end - # Make sure this isn't a deploy key - unless key.type.nil? + if key.is_a?(DeployKey) return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } end - unless user.present? + user = key.user + + unless user return { success: false, message: 'Could not find a user for the given key' } end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 077258faee1..c9689e6f8ef 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -41,7 +41,8 @@ module API issues = current_user.issues.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues.reorder(issuable_order_by => issuable_sort) + issues = issues.reorder(issuable_order_by => issuable_sort) + present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -73,7 +74,11 @@ module API params[:group_id] = group.id params[:milestone_title] = params.delete(:milestone) params[:label_name] = params.delete(:labels) - params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort] + + if params[:order_by] || params[:sort] + # The Sortable concern takes 'created_desc', not 'created_at_desc' (for example) + params[:sort] = "#{issuable_order_by.sub('_at', '')}_#{issuable_sort}" + end issues = IssuesFinder.new(current_user, params).execute @@ -113,7 +118,8 @@ module API issues = filter_issues_milestone(issues, params[:milestone]) end - issues.reorder(issuable_order_by => issuable_sort) + issues = issues.reorder(issuable_order_by => issuable_sort) + present paginate(issues), with: Entities::Issue, current_user: current_user end @@ -140,12 +146,13 @@ module API # labels (optional) - The labels of an issue # created_at (optional) - Date time string, ISO 8601 formatted # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential # Example Request: # POST /projects/:id/issues post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) @@ -154,21 +161,19 @@ module API render_api_error!({ labels: errors }, 400) end - project = user_project + attrs[:labels] = params[:labels] if params[:labels] + + # Convert and filter out invalid confidential flags + attrs['confidential'] = to_boolean(attrs['confidential']) + attrs.delete('confidential') if attrs['confidential'].nil? - 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) @@ -188,12 +193,13 @@ module API # state_event (optional) - The state event of an issue (close|reopen) # updated_at (optional) - Date time string, ISO 8601 formatted # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential # Example Request: # PUT /projects/:id/issues/:issue_id put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential] keys << :updated_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) @@ -202,17 +208,15 @@ module API render_api_error!({ labels: errors }, 400) end + attrs[:labels] = params[:labels] if params[:labels] + + # Convert and filter out invalid confidential flags + attrs['confidential'] = to_boolean(attrs['confidential']) + attrs.delete('confidential') if attrs['confidential'].nil? + 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/lint.rb b/lib/api/lint.rb new file mode 100644 index 00000000000..ae43a4a3237 --- /dev/null +++ b/lib/api/lint.rb @@ -0,0 +1,21 @@ +module API + class Lint < Grape::API + namespace :ci do + desc 'Validation of .gitlab-ci.yml content' + params do + requires :content, type: String, desc: 'Content of .gitlab-ci.yml' + end + post '/lint' do + error = Ci::GitlabCiYamlProcessor.validation_message(params[:content]) + + status 200 + + if error.blank? + { status: 'valid', errors: [] } + else + { status: 'invalid', errors: [error] } + end + end + end + end +end 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/notification_settings.rb b/lib/api/notification_settings.rb new file mode 100644 index 00000000000..a70a7e71073 --- /dev/null +++ b/lib/api/notification_settings.rb @@ -0,0 +1,97 @@ +module API + # notification_settings API + class NotificationSettings < Grape::API + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + resource :notification_settings do + desc 'Get global notification level settings and email, defaults to Participate' do + detail 'This feature was introduced in GitLab 8.12' + success Entities::GlobalNotificationSetting + end + get do + notification_setting = current_user.global_notification_setting + + present notification_setting, with: Entities::GlobalNotificationSetting + end + + desc 'Update global notification level settings and email, defaults to Participate' do + detail 'This feature was introduced in GitLab 8.12' + success Entities::GlobalNotificationSetting + end + params do + optional :level, type: String, desc: 'The global notification level' + optional :notification_email, type: String, desc: 'The email address to send notifications' + NotificationSetting::EMAIL_EVENTS.each do |event| + optional event, type: Boolean, desc: 'Enable/disable this notification' + end + end + put do + notification_setting = current_user.global_notification_setting + + begin + notification_setting.transaction do + new_notification_email = params.delete(:notification_email) + declared_params = declared(params, include_missing: false).to_h + + current_user.update(notification_email: new_notification_email) if new_notification_email + notification_setting.update(declared_params) + end + rescue ArgumentError => e # catch level enum error + render_api_error! e.to_s, 400 + end + + render_validation_error! current_user + render_validation_error! notification_setting + present notification_setting, with: Entities::GlobalNotificationSetting + end + end + + %w[group project].each do |source_type| + resource source_type.pluralize do + desc "Get #{source_type} level notification level settings, defaults to Global" do + detail 'This feature was introduced in GitLab 8.12' + success Entities::NotificationSetting + end + params do + requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME' + end + get ":id/notification_settings" do + source = find_source(source_type, params[:id]) + + notification_setting = current_user.notification_settings_for(source) + + present notification_setting, with: Entities::NotificationSetting + end + + desc "Update #{source_type} level notification level settings, defaults to Global" do + detail 'This feature was introduced in GitLab 8.12' + success Entities::NotificationSetting + end + params do + requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME' + optional :level, type: String, desc: "The #{source_type} notification level" + NotificationSetting::EMAIL_EVENTS.each do |event| + optional event, type: Boolean, desc: 'Enable/disable this notification' + end + end + put ":id/notification_settings" do + source = find_source(source_type, params.delete(:id)) + notification_setting = current_user.notification_settings_for(source) + + begin + declared_params = declared(params, include_missing: false).to_h + + notification_setting.update(declared_params) + rescue ArgumentError => e # catch level enum error + render_api_error! e.to_s, 400 + end + + render_validation_error! notification_setting + present notification_setting, with: Entities::NotificationSetting + end + end + end + end +end diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 2aae75c471d..2a0c8e1f2c0 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -13,11 +13,14 @@ module API params do optional :page, type: Integer, desc: 'Page number of the current request' optional :per_page, type: Integer, desc: 'Number of items per page' + optional :scope, type: String, values: ['running', 'branches', 'tags'], + desc: 'Either running, branches, or tags' end get ':id/pipelines' do authorize! :read_pipeline, user_project - present paginate(user_project.pipelines), with: Entities::Pipeline + pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) + present paginate(pipelines), with: Entities::Pipeline end desc 'Gets a specific pipeline for the project' do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 71efd4f33ca..644d836ed0b 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -51,7 +51,7 @@ module API @projects = current_user.viewable_starred_projects @projects = filter_projects(@projects) @projects = paginate @projects - present @projects, with: Entities::Project + present @projects, with: Entities::Project, user: current_user end # Get all projects for admin user @@ -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 @@ -124,7 +125,8 @@ module API :visibility_level, :import_url, :public_builds, - :only_allow_merge_if_build_succeeds] + :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? @@ -156,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 @@ -174,7 +177,8 @@ module API :visibility_level, :import_url, :public_builds, - :only_allow_merge_if_build_succeeds] + :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? @@ -185,16 +189,30 @@ module API end end - # Fork new project for the current user. + # Fork new project for the current user or provided namespace. # # Parameters: # id (required) - The ID of a project + # namespace (optional) - The ID or name of the namespace that the project will be forked into. # Example Request # POST /projects/fork/:id post 'fork/:id' do + attrs = {} + namespace_id = params[:namespace] + + if namespace_id.present? + namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id) + + not_found!('Target Namespace') unless namespace + + attrs[:namespace] = namespace + end + @forked_project = ::Projects::ForkService.new(user_project, - current_user).execute + current_user, + attrs).execute + if @forked_project.errors.any? conflict!(@forked_project.errors.messages) else @@ -220,6 +238,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 @@ -237,7 +256,8 @@ module API :public, :visibility_level, :public_builds, - :only_allow_merge_if_build_succeeds] + :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? @@ -408,18 +428,9 @@ module API # Example Request: # GET /projects/search/:query get "/search/:query" do - ids = current_user.authorized_projects.map(&:id) - visibility_levels = [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ] - projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%") - sort = params[:sort] == 'desc' ? 'desc' : 'asc' - - projects = case params["order_by"] - when 'id' then projects.order("id #{sort}") - when 'name' then projects.order("name #{sort}") - when 'created_at' then projects.order("created_at #{sort}") - when 'last_activity_at' then projects.order("last_activity_at #{sort}") - else projects - end + search_service = Search::GlobalService.new(current_user, search: params[:query]).execute + projects = search_service.objects('projects', params[:page]) + projects = projects.reorder(project_order_by => project_sort) present paginate(projects), with: Entities::Project end diff --git a/lib/api/users.rb b/lib/api/users.rb index 8a376d3c2a3..c440305ff0f 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -327,7 +327,7 @@ module API # Example Request: # GET /user get do - present @current_user, with: Entities::UserLogin + present @current_user, with: Entities::UserFull end # Get currently authenticated user's keys 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/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index d77a5e3ff09..16cd774c81a 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -18,10 +18,6 @@ module Banzai @object_sym ||= object_name.to_sym end - def self.object_class_title - @object_title ||= object_class.name.titleize - end - # Public: Find references in text (like `!123` for merge requests) # # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| @@ -49,10 +45,6 @@ module Banzai self.class.object_sym end - def object_class_title - self.class.object_class_title - end - def references_in(*args, &block) self.class.references_in(*args, &block) end @@ -198,7 +190,7 @@ module Banzai end def object_link_title(object) - "#{object_class_title}: #{object.title}" + object.title end def object_link_text(object, matches) diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index bbb88c979cc..4358bf45549 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -35,7 +35,7 @@ module Banzai end def object_link_title(range) - range.reference_title + nil end end end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 2ce1816672b..a26dd09c25a 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -28,10 +28,6 @@ module Banzai only_path: context[:only_path]) end - def object_link_title(commit) - commit.link_title - end - def object_link_text_extras(object, matches) extras = super diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index e258dc8e2bf..8f262ef3d8d 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -70,6 +70,11 @@ module Banzai def unescape_html_entities(text) CGI.unescapeHTML(text.to_s) end + + def object_link_title(object) + # use title of wrapped element instead + nil + end end end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index ca686c87d97..58fff496d00 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -59,6 +59,10 @@ module Banzai html_safe end end + + def object_link_title(object) + nil + end end end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index bf058241cda..2d221290f7e 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -52,7 +52,7 @@ module Banzai end def reference_class(type) - "gfm gfm-#{type}" + "gfm gfm-#{type} has-tooltip" end # Ensure that a :project key exists in context diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 6cf218aaa0d..e8e03e4a98f 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -211,7 +211,7 @@ module Banzai end def can?(user, permission, subject) - Ability.abilities.allowed?(user, permission, subject) + Ability.allowed?(user, permission, subject) end def find_projects_for_hash_keys(hash) diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 9f3b582a263..54db63d4628 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -12,7 +12,7 @@ module Ci # POST /builds/register post "register" do authenticate_runner! - update_runner_last_contact + update_runner_last_contact(save: false) update_runner_info required_attributes! [:token] not_found! unless current_runner.active? @@ -101,6 +101,7 @@ module Ci # POST /builds/:id/artifacts/authorize post ":id/artifacts/authorize" do require_gitlab_workhorse! + Gitlab::Workhorse.verify_api_request!(headers) not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) not_found! unless build @@ -113,7 +114,8 @@ module Ci end status 200 - { TempPath: ArtifactUploader.artifacts_upload_path } + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + Gitlab::Workhorse.artifact_upload_ok end # Upload artifacts to build - Runners only diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 199d62d9b8a..bcabf7a21b2 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -3,7 +3,7 @@ module Ci module Helpers BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN" BUILD_TOKEN_PARAM = :token - UPDATE_RUNNER_EVERY = 60 + UPDATE_RUNNER_EVERY = 40 * 60 def authenticate_runners! forbidden! unless runner_registration_token_valid? @@ -22,11 +22,13 @@ module Ci params[:token] == current_application_settings.runners_registration_token end - def update_runner_last_contact + def update_runner_last_contact(save: true) # Use a random threshold to prevent beating DB updates + # it generates a distribution between: [40m, 80m] contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age - current_runner.update_attributes(contacted_at: Time.now) + current_runner.contacted_at = Time.now + current_runner.save if current_runner.changed? && save end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 47efd5bd9f2..caa815f720f 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -55,12 +55,7 @@ module Ci { stage_idx: @stages.index(job[:stage]), stage: job[:stage], - ## - # Refactoring note: - # - before script behaves differently than after script - # - after script returns an array of commands - # - before script should be a concatenated command - commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"), + commands: job[:commands], tag_list: job[:tags] || [], name: job[:name].to_s, allow_failure: job[:allow_failure] || false, @@ -68,16 +63,27 @@ module Ci environment: job[:environment], yaml_variables: yaml_variables(name), options: { - image: job[:image] || @image, - services: job[:services] || @services, + image: job[:image], + services: job[:services], artifacts: job[:artifacts], - cache: job[:cache] || @cache, + cache: job[:cache], dependencies: job[:dependencies], - after_script: job[:after_script] || @after_script, + after_script: job[:after_script], }.compact } end + def self.validation_message(content) + return 'Please provide content of .gitlab-ci.yml' if content.blank? + + begin + Ci::GitlabCiYamlProcessor.new(content) + nil + rescue ValidationError, Psych::SyntaxError => e + e.message + end + end + private def initial_parsing diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 95d925dc7f3..9a0482306b7 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -12,9 +12,7 @@ module Gitlab @ref = ref @job = job - @pipeline = @project.pipelines - .latest_successful_for(@ref) - .first + @pipeline = @project.pipelines.latest_successful_for(@ref) end def entity diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 7beaecd1cf0..f4b5097adb1 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -21,7 +21,7 @@ module Gitlab private - def gl_user_id(project, bitbucket_id) + def gitlab_user_id(project, bitbucket_id) if bitbucket_id user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) (user && user.id) || project.creator_id @@ -74,7 +74,7 @@ module Gitlab description: body, title: issue["title"], state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', - author_id: gl_user_id(project, reporter) + author_id: gitlab_user_id(project, reporter) ) end rescue ActiveRecord::RecordInvalid => e diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index ae82c0db3f1..bbfa6cf7d05 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -14,7 +14,7 @@ module Gitlab @config = Loader.new(config).load! @global = Node::Global.new(@config) - @global.process! + @global.compose! end def valid? diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb index 2de82d40c9d..6b7ab2fdaf2 100644 --- a/lib/gitlab/ci/config/node/configurable.rb +++ b/lib/gitlab/ci/config/node/configurable.rb @@ -23,9 +23,9 @@ module Gitlab end end - private + def compose!(deps = nil) + return unless valid? - def compose! self.class.nodes.each do |key, factory| factory .value(@config[key]) @@ -33,6 +33,12 @@ module Gitlab @entries[key] = factory.create! end + + yield if block_given? + + @entries.each_value do |entry| + entry.compose!(deps) + end end class_methods do diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb index 0c782c422b5..8717eabf81e 100644 --- a/lib/gitlab/ci/config/node/entry.rb +++ b/lib/gitlab/ci/config/node/entry.rb @@ -20,11 +20,14 @@ module Gitlab @validator.validate(:new) end - def process! + def [](key) + @entries[key] || Node::Undefined.new + end + + def compose!(deps = nil) return unless valid? - compose! - descendants.each(&:process!) + yield if block_given? end def leaf? @@ -73,11 +76,6 @@ module Gitlab def self.validator Validator end - - private - - def compose! - end end end end diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb index 707b052e6a8..5387f29ad59 100644 --- a/lib/gitlab/ci/config/node/factory.rb +++ b/lib/gitlab/ci/config/node/factory.rb @@ -37,8 +37,8 @@ module Gitlab # See issue #18775. # if @value.nil? - Node::Undefined.new( - fabricate_undefined + Node::Unspecified.new( + fabricate_unspecified ) else fabricate(@node, @value) @@ -47,13 +47,13 @@ module Gitlab private - def fabricate_undefined + def fabricate_unspecified ## # If node has a default value we fabricate concrete node # with default value. # if @node.default.nil? - fabricate(Node::Null) + fabricate(Node::Undefined) else fabricate(@node, @node.default) end diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb index ccd539fb003..2a2943c9288 100644 --- a/lib/gitlab/ci/config/node/global.rb +++ b/lib/gitlab/ci/config/node/global.rb @@ -36,15 +36,15 @@ module Gitlab helpers :before_script, :image, :services, :after_script, :variables, :stages, :types, :cache, :jobs - private - - def compose! - super - - compose_jobs! - compose_deprecated_entries! + def compose!(_deps = nil) + super(self) do + compose_jobs! + compose_deprecated_entries! + end end + private + def compose_jobs! factory = Node::Factory.new(Node::Jobs) .value(@config.except(*self.class.nodes.keys)) 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/job.rb b/lib/gitlab/ci/config/node/job.rb index e84737acbb9..0cbdf7619c0 100644 --- a/lib/gitlab/ci/config/node/job.rb +++ b/lib/gitlab/ci/config/node/job.rb @@ -80,7 +80,19 @@ module Gitlab helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts + :artifacts, :commands + + def compose!(deps = nil) + super do + if type_defined? && !stage_defined? + @entries[:stage] = @entries[:type] + end + + @entries.delete(:type) + end + + inherit!(deps) + end def name @metadata[:name] @@ -90,12 +102,30 @@ module Gitlab @config.merge(to_hash.compact) end + def commands + (before_script_value.to_a + script_value.to_a).join("\n") + end + private + def inherit!(deps) + return unless deps + + self.class.nodes.each_key do |key| + global_entry = deps[key] + job_entry = @entries[key] + + if global_entry.specified? && !job_entry.specified? + @entries[key] = global_entry + end + end + end + def to_hash { name: name, before_script: before_script, script: script, + commands: commands, image: image, services: services, stage: stage, @@ -106,16 +136,6 @@ module Gitlab artifacts: artifacts, after_script: after_script } end - - def compose! - super - - if type_defined? && !stage_defined? - @entries[:stage] = @entries[:type] - end - - @entries.delete(:type) - end end end end diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb index 51683c82ceb..d10e80d1a7d 100644 --- a/lib/gitlab/ci/config/node/jobs.rb +++ b/lib/gitlab/ci/config/node/jobs.rb @@ -26,19 +26,23 @@ module Gitlab name.to_s.start_with?('.') end - private - - def compose! - @config.each do |name, config| - node = hidden?(name) ? Node::HiddenJob : Node::Job - - factory = Node::Factory.new(node) - .value(config || {}) - .metadata(name: name) - .with(key: name, parent: self, - description: "#{name} job definition.") + def compose!(deps = nil) + super do + @config.each do |name, config| + node = hidden?(name) ? Node::Hidden : Node::Job + + factory = Node::Factory.new(node) + .value(config || {}) + .metadata(name: name) + .with(key: name, parent: self, + description: "#{name} job definition.") + + @entries[name] = factory.create! + end - @entries[name] = factory.create! + @entries.each_value do |entry| + entry.compose!(deps) + end end end end diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb deleted file mode 100644 index 88a5f53f13c..00000000000 --- a/lib/gitlab/ci/config/node/null.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Gitlab - module Ci - class Config - module Node - ## - # This class represents an undefined node. - # - # Implements the Null Object pattern. - # - class Null < Entry - def value - nil - end - - def valid? - true - end - - def errors - [] - end - - def specified? - false - end - - def relevant? - false - end - end - end - end - end -end diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb index 45fef8c3ae5..33e78023539 100644 --- a/lib/gitlab/ci/config/node/undefined.rb +++ b/lib/gitlab/ci/config/node/undefined.rb @@ -3,15 +3,34 @@ module Gitlab class Config module Node ## - # This class represents an unspecified entry node. + # This class represents an undefined node. # - # It decorates original entry adding method that indicates it is - # unspecified. + # Implements the Null Object pattern. # - class Undefined < SimpleDelegator + class Undefined < Entry + def initialize(*) + super(nil) + end + + def value + nil + end + + def valid? + true + end + + def errors + [] + end + def specified? false end + + def relevant? + false + end end end end diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/node/unspecified.rb new file mode 100644 index 00000000000..a7d1f6131b8 --- /dev/null +++ b/lib/gitlab/ci/config/node/unspecified.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + class Config + module Node + ## + # This class represents an unspecified entry node. + # + # It decorates original entry adding method that indicates it is + # unspecified. + # + class Unspecified < SimpleDelegator + def specified? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb new file mode 100644 index 00000000000..a210e76acaa --- /dev/null +++ b/lib/gitlab/ci/pipeline_duration.rb @@ -0,0 +1,141 @@ +module Gitlab + module Ci + # # Introduction - total running time + # + # The problem this module is trying to solve is finding the total running + # time amongst all the jobs, excluding retries and pending (queue) time. + # We could reduce this problem down to finding the union of periods. + # + # So each job would be represented as a `Period`, which consists of + # `Period#first` as when the job started and `Period#last` as when the + # job was finished. A simple example here would be: + # + # * A (1, 3) + # * B (2, 4) + # * C (6, 7) + # + # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4. + # C begins from 6, and ends to 7. Visually it could be viewed as: + # + # 0 1 2 3 4 5 6 7 + # AAAAAAA + # BBBBBBB + # CCCC + # + # The union of A, B, and C would be (1, 4) and (6, 7), therefore the + # total running time should be: + # + # (4 - 1) + (7 - 6) => 4 + # + # # The Algorithm + # + # The algorithm used here for union would be described as follow. + # First we make sure that all periods are sorted by `Period#first`. + # Then we try to merge periods by iterating through the first period + # to the last period. The goal would be merging all overlapped periods + # so that in the end all the periods are discrete. When all periods + # are discrete, we're free to just sum all the periods to get real + # running time. + # + # Here we begin from A, and compare it to B. We could find that + # before A ends, B already started. That is `B.first <= A.last` + # that is `2 <= 3` which means A and B are overlapping! + # + # When we found that two periods are overlapping, we would need to merge + # them into a new period and disregard the old periods. To make a new + # period, we take `A.first` as the new first because remember? we sorted + # them, so `A.first` must be smaller or equal to `B.first`. And we take + # `[A.last, B.last].max` as the new last because we want whoever ended + # later. This could be broken into two cases: + # + # 0 1 2 3 4 + # AAAAAAA + # BBBBBBB + # + # Or: + # + # 0 1 2 3 4 + # AAAAAAAAAA + # BBBB + # + # So that we need to take whoever ends later. Back to our example, + # after merging and discard A and B it could be visually viewed as: + # + # 0 1 2 3 4 5 6 7 + # DDDDDDDDDD + # CCCC + # + # Now we could go on and compare the newly created D and the old C. + # We could figure out that D and C are not overlapping by checking + # `C.first <= D.last` is `false`. Therefore we need to keep both C + # and D. The example would end here because there are no more jobs. + # + # After having the union of all periods, we just need to sum the length + # of all periods to get total time. + # + # (4 - 1) + (7 - 6) => 4 + # + # That is 4 is the answer in the example. + module PipelineDuration + extend self + + Period = Struct.new(:first, :last) do + def duration + last - first + end + end + + def from_pipeline(pipeline) + status = %w[success failed running canceled] + builds = pipeline.builds.latest. + where(status: status).where.not(started_at: nil).order(:started_at) + + from_builds(builds) + end + + def from_builds(builds) + now = Time.now + + periods = builds.map do |b| + Period.new(b.started_at, b.finished_at || now) + end + + from_periods(periods) + end + + # periods should be sorted by `first` + def from_periods(periods) + process_duration(process_periods(periods)) + end + + private + + def process_periods(periods) + return periods if periods.empty? + + periods.drop(1).inject([periods.first]) do |result, current| + previous = result.last + + if overlap?(previous, current) + result[-1] = merge(previous, current) + result + else + result << current + end + end + end + + def overlap?(previous, current) + current.first <= previous.last + end + + def merge(previous, current) + Period.new(previous.first, [previous.last, current.last].max) + end + + def process_duration(periods) + periods.sum(&:duration) + end + end + end +end 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..98e842cded3 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -13,9 +13,18 @@ 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 + raise UnmergeableFile if text.length > 200.kilobytes + + begin + text.to_json + rescue Encoding::UndefinedConversionError + raise UnsupportedEncoding + end line_obj_index = 0 line_old = 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 27acd817e51..12fbb78c53e 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -41,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/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 72992baffd4..d546e102c63 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -15,7 +15,7 @@ module Gitlab private - def gl_user_id(github_id) + def gitlab_user_id(github_id) User.joins(:identities). find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). try(:id) diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 2c1b94ef2cd..1c7c1a73c77 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -21,7 +21,7 @@ module Gitlab end def author_id - gl_user_id(raw_data.user.id) || project.creator_id + gitlab_user_id(raw_data.user.id) || project.creator_id end def body diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 02ffb43d89b..0388c58f811 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -133,8 +133,7 @@ module Gitlab if issue.labels.count > 0 label_ids = issue.labels - .map { |raw| LabelFormatter.new(project, raw).attributes } - .map { |attrs| Label.find_by(attrs).try(:id) } + .map { |attrs| project.labels.find_by(title: attrs.name).try(:id) } .compact issuable.update_attribute(:label_ids, label_ids) @@ -152,12 +151,14 @@ module Gitlab end def create_comments(issuable, comments) - comments.each do |raw| - 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 } + ActiveRecord::Base.no_touching do + comments.each do |raw| + 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 end @@ -166,7 +167,7 @@ module Gitlab unless project.wiki_enabled? wiki = WikiFormatter.new(project) gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url) - project.update_attribute(:wiki_enabled, true) + project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) end rescue Gitlab::Shell::Error => e # GitHub error message when the wiki repo has not been created, diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index 835ec858b35..ad4f1d8ae99 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -12,7 +12,7 @@ module Gitlab author_id: author_id, assignee_id: assignee_id, created_at: raw_data.created_at, - updated_at: updated_at + updated_at: raw_data.updated_at } end @@ -40,7 +40,7 @@ module Gitlab def assignee_id if assigned? - gl_user_id(raw_data.assignee.id) + gitlab_user_id(raw_data.assignee.id) end end @@ -49,7 +49,7 @@ module Gitlab end def author_id - gl_user_id(raw_data.user.id) || project.creator_id + gitlab_user_id(raw_data.user.id) || project.creator_id end def body @@ -69,10 +69,6 @@ module Gitlab def state raw_data.state == 'closed' ? 'closed' : 'opened' end - - def updated_at - state == 'closed' ? raw_data.closed_at : raw_data.updated_at - end end end end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 9f18244e7d7..2cad7fca88e 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -13,6 +13,12 @@ module Gitlab Label end + def create! + project.labels.find_or_create_by!(title: title) do |label| + label.color = color + end + end + private def color diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index 53d4b3102d1..b2fa524cf5b 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -3,14 +3,14 @@ module Gitlab class MilestoneFormatter < BaseFormatter def attributes { - iid: number, + iid: raw_data.number, project: project, - title: title, - description: description, - due_date: due_date, + title: raw_data.title, + description: raw_data.description, + due_date: raw_data.due_on, state: state, - created_at: created_at, - updated_at: updated_at + created_at: raw_data.created_at, + updated_at: raw_data.updated_at } end @@ -20,33 +20,9 @@ module Gitlab private - def number - raw_data.number - end - - def title - raw_data.title - end - - def description - raw_data.description - end - - def due_date - raw_data.due_on - end - def state raw_data.state == 'closed' ? 'closed' : 'active' end - - def created_at - raw_data.created_at - end - - def updated_at - state == 'closed' ? raw_data.closed_at : raw_data.updated_at - end end end end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index f4221003db5..e9725880c5e 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -11,18 +11,24 @@ module Gitlab end def execute - ::Projects::CreateService.new( + project = ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, description: repo.description, namespace_id: namespace.id, - visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility, import_type: "github", import_source: repo.full_name, - import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"), - wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later + import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") ).execute + + # If repo has wiki we'll import it later + if repo.has_wiki? && project + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end + + project end end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 04aa3664f64..87e031b27f8 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -20,7 +20,7 @@ module Gitlab author_id: author_id, assignee_id: assignee_id, created_at: raw_data.created_at, - updated_at: updated_at + updated_at: raw_data.updated_at } end @@ -68,7 +68,7 @@ module Gitlab def assignee_id if assigned? - gl_user_id(raw_data.assignee.id) + gitlab_user_id(raw_data.assignee.id) end end @@ -77,7 +77,7 @@ module Gitlab end def author_id - gl_user_id(raw_data.user.id) || project.creator_id + gitlab_user_id(raw_data.user.id) || project.creator_id end def body @@ -103,15 +103,6 @@ module Gitlab 'opened' end end - - def updated_at - case state - when 'merged' then raw_data.merged_at - when 'closed' then raw_data.closed_at - else - raw_data.updated_at - end - end end end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 46d40f75be6..e44d7934fda 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -41,7 +41,8 @@ module Gitlab title: issue["title"], state: issue["state"], updated_at: issue["updated_at"], - author_id: gl_user_id(project, issue["author"]["id"]) + author_id: gitlab_user_id(project, issue["author"]["id"]), + confidential: issue["confidential"] ) end end @@ -51,7 +52,7 @@ module Gitlab private - def gl_user_id(project, gitlab_id) + def gitlab_user_id(project, gitlab_id) user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s) (user && user.id) || project.creator_id end 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_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 1da51043611..c2e8a1ca5dd 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -39,15 +39,12 @@ project_tree: - :labels - milestones: - :events + - :project_feature # Only include the following attributes for the models specified. included_attributes: project: - :description - - :issues_enabled - - :merge_requests_enabled - - :wiki_enabled - - :snippets_enabled - :visibility_level - :archived user: @@ -72,4 +69,4 @@ methods: statuses: - :type merge_request_diff: - - :utf8_st_diffs
\ No newline at end of file + - :utf8_st_diffs 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/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 9a5bcfb5c9b..9100719da87 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -23,31 +23,7 @@ module Gitlab end def users(field, value, limit = nil) - if field.to_sym == :dn - options = { - base: value, - scope: Net::LDAP::SearchScope_BaseObject - } - else - options = { - base: config.base, - filter: Net::LDAP::Filter.eq(field, value) - } - end - - if config.user_filter.present? - user_filter = Net::LDAP::Filter.construct(config.user_filter) - - options[:filter] = if options[:filter] - Net::LDAP::Filter.join(options[:filter], user_filter) - else - user_filter - end - end - - if limit.present? - options.merge!(size: limit) - end + options = user_options(field, value, limit) entries = ldap_search(options).select do |entry| entry.respond_to? config.uid @@ -90,6 +66,38 @@ module Gitlab Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds") [] end + + private + + def user_options(field, value, limit) + options = { attributes: %W(#{config.uid} cn mail dn) } + options[:size] = limit if limit + + if field.to_sym == :dn + options[:base] = value + options[:scope] = Net::LDAP::SearchScope_BaseObject + options[:filter] = user_filter + else + options[:base] = config.base + options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value)) + end + + options + end + + def user_filter(filter = nil) + if config.user_filter.present? + user_filter = Net::LDAP::Filter.construct(config.user_filter) + end + + if user_filter && filter + Net::LDAP::Filter.join(filter, user_filter) + elsif user_filter + user_filter + else + filter + end + end end end 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/popen.rb b/lib/gitlab/popen.rb index ca23ccef25b..a0fd41161a5 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -21,9 +21,9 @@ module Gitlab @cmd_output = "" @cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - # We are not using stdin so we should close it, in case the command we - # are running waits for input. + yield(stdin) if block_given? stdin.close + @cmd_output << stdout.read @cmd_output << stderr.read @cmd_status = wait_thr.value.exitstatus diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 183bd10d6a3..5b9cfaeb2f8 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -28,11 +28,6 @@ module Gitlab end end - def total_count - @total_count ||= issues_count + merge_requests_count + blobs_count + - notes_count + wiki_blobs_count + commits_count - end - def blobs_count @blobs_count ||= blobs.count end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index f8ab2b1f09e..2690938fe82 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -27,11 +27,6 @@ module Gitlab end end - def total_count - @total_count ||= projects_count + issues_count + merge_requests_count + - milestones_count - end - def projects_count @projects_count ||= projects.count end @@ -48,10 +43,6 @@ module Gitlab @milestones_count ||= milestones.count end - def empty? - total_count.zero? - end - private def projects diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb new file mode 100644 index 00000000000..117fc508135 --- /dev/null +++ b/lib/gitlab/sentry.rb @@ -0,0 +1,27 @@ +module Gitlab + module Sentry + def self.enabled? + Rails.env.production? && current_application_settings.sentry_enabled? + end + + def self.context(current_user = nil) + return unless self.enabled? + + if current_user + Raven.user_context( + id: current_user.id, + email: current_user.email, + username: current_user.username, + ) + end + end + + def self.program_context + if Sidekiq.server? + 'sidekiq' + else + 'rails' + end + end + end +end diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index e0e74ff8359..9e01f02029c 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -20,10 +20,6 @@ module Gitlab end end - def total_count - @total_count ||= snippet_titles_count + snippet_blobs_count - end - def snippet_titles_count @snippet_titles_count ||= snippet_titles.count end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index c6826a09bd2..efe4aeb399d 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -1,19 +1,38 @@ require 'base64' require 'json' +require 'securerandom' module Gitlab class Workhorse SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data' VERSION_FILE = 'GITLAB_WORKHORSE_VERSION' + INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json' + INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request' + + # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32 + # bytes https://tools.ietf.org/html/rfc4868#section-2.6 + SECRET_LENGTH = 32 class << self def git_http_ok(repository, user) { - 'GL_ID' => Gitlab::GlId.gl_id(user), - 'RepoPath' => repository.path_to_repo, + GL_ID: Gitlab::GlId.gl_id(user), + RepoPath: repository.path_to_repo, } end + def lfs_upload_ok(oid, size) + { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + } + end + + def artifact_upload_ok + { TempPath: ArtifactUploader.artifacts_upload_path } + end + def send_git_blob(repository, blob) params = { 'RepoPath' => repository.path_to_repo, @@ -81,6 +100,35 @@ module Gitlab path.readable? ? path.read.chomp : 'unknown' end + def secret + @secret ||= begin + bytes = Base64.strict_decode64(File.read(secret_path)) + raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH + bytes + end + end + + def write_secret + bytes = SecureRandom.random_bytes(SECRET_LENGTH) + File.open(secret_path, 'w:BINARY', 0600) do |f| + f.chmod(0600) + f.write(Base64.strict_encode64(bytes)) + end + end + + def verify_api_request!(request_headers) + JWT.decode( + request_headers[INTERNAL_API_REQUEST_HEADER], + secret, + true, + { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' }, + ) + end + + def secret_path + Rails.root.join('.gitlab_workhorse_secret') + end + protected def encode(hash) diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 07bf8d2d1c3..1d3c9fbbe2f 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -146,21 +146,42 @@ describe Import::BitbucketController do end context "when a namespace with the Bitbucket user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 51d59526854..ebfbf54182b 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -181,21 +181,42 @@ describe Import::GithubController do end context "when a namespace with the GitHub user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(github_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index e8cf6aa7767..6f75ebb16c8 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -136,21 +136,42 @@ describe Import::GitlabController do end context "when a namespace with the GitLab.com user's username doesn't exist" do - it "creates the namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) - post :create, format: :js + expect { post :create, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). + and_return(double(execute: true)) - expect(Namespace.where(name: other_username).first).not_to be_nil + post :create, format: :js + end end - it "takes the new namespace" do - expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end - post :create, format: :js + it "doesn't create the namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(gitlab_repo, user.namespace, user, access_params). + and_return(double(execute: true)) + + post :create, format: :js + end end end end 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/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index d0ad5e26dbd..2896636db5a 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -41,8 +41,8 @@ describe Projects::Boards::IssuesController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false) end it 'returns a successful 403 response' do diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 9496636e3cc..d687dea3c3b 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -35,11 +35,11 @@ describe Projects::Boards::ListsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).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/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 75a6d39e82c..6f6e608e1f3 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -23,8 +23,8 @@ describe Projects::BoardsController do context 'with unauthorized user' do before do - allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false) + allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) + allow(Ability).to receive(:allowed?).with(user, :read_board, project).and_return(false) end it 'returns a successful 404 response' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0836b71056c..90419368f22 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 @@ -370,6 +370,12 @@ describe Projects::IssuesController do expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now end + + it 'delegates the update of the todos count cache to TodoService' do + expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once + + delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c64c2b075c5..94c9edc91fe 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 @@ -291,6 +320,12 @@ describe Projects::MergeRequestsController do expect(response).to have_http_status(302) expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now end + + it 'delegates the update of the todos count cache to TodoService' do + expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once + + delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid + end end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index cccd492ef06..2e44b5128b4 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -49,4 +49,20 @@ describe Projects::ServicesController do let!(:referrer) { nil } end end + + describe 'PUT #update' do + context 'on successful update' do + it 'sets the flash' do + expect(service).to receive(:to_param).and_return('hipchat') + + put :update, + namespace_id: project.namespace.id, + project_id: project.id, + id: service.id, + service: { active: false } + + expect(flash[:notice]).to eq 'Successfully updated.' + end + end + end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index b8a28f43707..72a3ebf2ebd 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::SnippetsController do - let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } + let(:project) { create(:project_empty_repo, :public) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 4e9bfb0c69b..8f27e616c3e 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -136,6 +136,29 @@ describe SessionsController do post(:create, { user: user_params }, { otp_user_id: user.id }) end + context 'remember_me field' do + it 'sets a remember_user_token cookie when enabled' do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + allow(controller).to receive(:find_user).and_return(user) + expect(controller). + to receive(:remember_me).with(user).and_call_original + + authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}") + + expect(response.cookies['remember_user_token']).to be_present + end + + it 'does nothing when disabled' do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + allow(controller).to receive(:find_user).and_return(user) + expect(controller).not_to receive(:remember_me) + + authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}") + + expect(response.cookies['remember_user_token']).to be_nil + end + end + it "creates an audit log record" do allow(U2fRegistration).to receive(:authenticate).and_return(true) expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1) diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83e38095feb..6919002dedc 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -28,6 +28,11 @@ FactoryGirl.define do diff_refs: noteable.diff_refs ) end + + trait :resolved do + resolved_at { Time.now } + resolved_by { create(:user) } + end end factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f82d68a1816..fb84ba07d25 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,7 +8,6 @@ FactoryGirl.define do path { name.downcase.gsub(/\s/, '_') } namespace creator - snippets_enabled true trait :public do visibility_level Gitlab::VisibilityLevel::PUBLIC @@ -27,6 +26,26 @@ FactoryGirl.define do project.create_repository end end + + # Nest Project Feature attributes + transient do + wiki_access_level ProjectFeature::ENABLED + builds_access_level ProjectFeature::ENABLED + snippets_access_level ProjectFeature::ENABLED + issues_access_level ProjectFeature::ENABLED + merge_requests_access_level ProjectFeature::ENABLED + end + + after(:create) do |project, evaluator| + project.project_feature. + update_attributes( + wiki_access_level: evaluator.wiki_access_level, + builds_access_level: evaluator.builds_access_level, + snippets_access_level: evaluator.snippets_access_level, + issues_access_level: evaluator.issues_access_level, + merge_requests_access_level: evaluator.merge_requests_access_level, + ) + end end # Project with empty repository 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 5d777895542..c6c2e2095df 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 @@ -143,14 +182,21 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('56') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 56 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource(spinner: false) - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 56 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + wait_for_vue_resource(spinner: false) + + expect(page).to have_selector('.card', count: 56) + expect(page).to have_content('Showing all issues') end end @@ -162,32 +208,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) @@ -466,13 +486,19 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('20') + expect(page.find('.board-header')).to have_content('51') expect(page).to have_selector('.card', count: 20) + expect(page).to have_content('Showing 20 of 51 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - expect(page.find('.board-header')).to have_content('40') expect(page).to have_selector('.card', count: 40) + expect(page).to have_content('Showing 40 of 51 issues') + + evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + + expect(page).to have_selector('.card', count: 51) + expect(page).to have_content('Showing all issues') end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 688f68d3cff..8863554ee91 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -211,6 +211,13 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding all diffs' do before do click_link('Expand all') + + # Wait for elements to appear to ensure full page reload + expect(page).to have_content('This diff was suppressed by a .gitattributes entry') + expect(page).to have_content('This diff could not be displayed because it is too large.') + expect(page).to have_content('too_large_image.jpg') + find('.note-textarea') + wait_for_ajax execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') end 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/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index e262f285868..0e9f814044e 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -8,6 +8,7 @@ describe 'Filter issues', feature: true do let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:issue1) { create(:issue, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do project.team << [user, :master] @@ -107,6 +108,15 @@ describe 'Filter issues', feature: true do end expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) end + + it 'filters by wont fix labels' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content wontfix.title + click_link wontfix.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title) + end end describe 'Filter issues for assignee and label from issues#index' do 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/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb new file mode 100644 index 00000000000..41f218eaa8b --- /dev/null +++ b/spec/features/issues/reset_filters_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} + let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} + + before do + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_issues(project, milestone_title: milestone.title) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_issues(project, label_name: bug.name) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_issues(project, issue_search: 'Bug') + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_issues(project, author_id: user.id) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_issues(project, assignee_id: user.id) + expect(page).to have_css('.issue', count: 1) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, issue_search: 'Bug') + expect(page).to have_css('.issue', count: 0) + + reset_filters + expect(page).to have_css('.issue', count: 2) + end + end + + def reset_filters + find('.reset-filters').click + end +end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 2883e392694..105629c485a 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' feature 'Issues > User uses slash commands', feature: true, js: true do + include SlashCommandsHelpers include WaitForAjax it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do @@ -17,14 +18,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do visit namespace_project_issue_path(project.namespace, project, issue) end + after do + wait_for_ajax + end + describe 'adding a due date from note' do let(:issue) { create(:issue, project: project) } it 'does not create a note, and sets the due date accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/due 2016-08-28" - click_button 'Comment' - end + write_note("/due 2016-08-28") expect(page).not_to have_content '/due 2016-08-28' expect(page).to have_content 'Your commands have been executed!' @@ -41,10 +43,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do it 'does not create a note, and removes the due date accordingly' do expect(issue.due_date).to eq Date.new(2016, 8, 28) - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/remove_due_date" - click_button 'Comment' - end + write_note("/remove_due_date") expect(page).not_to have_content '/remove_due_date' expect(page).to have_content 'Your commands have been executed!' 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 index a818679a874..06fad1007e8 100644 --- a/spec/features/merge_requests/diff_notes_spec.rb +++ b/spec/features/merge_requests/diff_notes_spec.rb @@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do 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 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..9e759de3752 --- /dev/null +++ b/spec/features/merge_requests/merge_request_versions_spec.rb @@ -0,0 +1,68 @@ +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-dropdown' do + expect(page).to have_content 'latest version' + end + + expect(page).to have_content '8 changed files' + end + + describe 'switch between versions' do + before do + page.within '.mr-version-dropdown' do + find('.btn-link').click + click_link 'version 1' + end + end + + it 'should show older version' do + page.within '.mr-version-dropdown' do + expect(page).to have_content 'version 1' + end + + expect(page).to have_content '5 changed files' + end + + it 'show the message about disabled comments' do + expect(page).to have_content 'Comments are disabled' + end + end + + describe 'compare with older version' do + before do + page.within '.mr-version-compare-dropdown' do + find('.btn-link').click + click_link 'version 1' + end + end + + it 'should has correct value in the compare dropdown' do + page.within '.mr-version-compare-dropdown' do + expect(page).to have_content 'version 1' + end + end + + it 'show the message about disabled comments' do + expect(page).to have_content 'Comments are disabled' + end + + it 'show diff between new and old version' do + expect(page).to have_content '4 changed files with 15 additions and 6 deletions' + end + + it 'show diff between new and old version' do + expect(page).to have_content '4 changed files with 15 additions and 6 deletions' + end + end +end diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb new file mode 100644 index 00000000000..b56fdfe5611 --- /dev/null +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +feature 'Multiple merge requests updating from merge_requests#index', feature: true do + include WaitForAjax + + let!(:user) { create(:user)} + let!(:project) { create(:project) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'status', js: true do + describe 'close merge request' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'closes merge request' do + change_status('Closed') + + expect(page).to have_selector('.merge-request', count: 0) + end + end + + describe 'reopen merge request' do + before do + merge_request.close + visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed') + end + + it 'reopens merge request' do + change_status('Open') + + expect(page).to have_selector('.merge-request', count: 0) + end + end + end + + context 'assignee', js: true do + describe 'set assignee' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "updates merge request with assignee" do + change_assignee(user.name) + + page.within('.merge-request .controls') do + expect(find('.author_link')["title"]).to have_content(user.name) + end + end + end + + describe 'remove assignee' do + before do + merge_request.assignee = user + merge_request.save + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "removes assignee from the merge request" do + change_assignee('Unassigned') + + expect(find('.merge-request .controls')).not_to have_css('.author_link') + end + end + end + + context 'milestone', js: true do + let(:milestone) { create(:milestone, project: project) } + + describe 'set milestone' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "updates merge request with milestone" do + change_milestone(milestone.title) + + expect(find('.merge-request')).to have_content milestone.title + end + end + + describe 'unset milestone' do + before do + merge_request.milestone = milestone + merge_request.save + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it "removes milestone from the merge request" do + change_milestone("No Milestone") + + expect(find('.merge-request')).not_to have_content milestone.title + end + end + end + + def change_status(text) + find('#check_all_issues').click + find('.js-issue-status').click + find('.dropdown-menu-status a', text: text).click + click_update_merge_requests_button + end + + def change_assignee(text) + find('#check_all_issues').click + find('.js-update-assignee').click + wait_for_ajax + + page.within '.dropdown-menu-user' do + click_link text + end + + click_update_merge_requests_button + end + + def change_milestone(text) + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + find('.dropdown-menu-milestone a', text: text).click + click_update_merge_requests_button + end + + def click_update_merge_requests_button + find('.update_selected_issues').click + wait_for_ajax + end +end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index d9ef0d18074..22d9d1b9fd5 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' feature 'Merge Requests > User uses slash commands', feature: true, js: true do + include SlashCommandsHelpers include WaitForAjax let(:user) { create(:user) } @@ -20,11 +21,12 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do visit namespace_project_merge_request_path(project.namespace, project, merge_request) end + after do + wait_for_ajax + end + it 'does not recognize the command nor create a note' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/due 2016-08-28" - click_button 'Comment' - end + write_note("/due 2016-08-28") expect(page).not_to have_content '/due 2016-08-28' 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/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 1b14945bf0a..d26a0caf036 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -1,32 +1,46 @@ require 'spec_helper' describe 'Branches', feature: true do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:repository) { project.repository } - before do - login_as :user - project.team << [@user, :developer] - end + context 'logged in' do + before do + login_as :user + project.team << [@user, :developer] + end - describe 'Initial branches page' do - it 'shows all the branches' do - visit namespace_project_branches_path(project.namespace, project) + describe 'Initial branches page' do + it 'shows all the branches' do + visit namespace_project_branches_path(project.namespace, project) - repository.branches { |branch| expect(page).to have_content("#{branch.name}") } - expect(page).to have_content("Protected branches can be managed in project settings") + repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + expect(page).to have_content("Protected branches can be managed in project settings") + end + end + + describe 'Find branches' do + it 'shows filtered branches', js: true do + visit namespace_project_branches_path(project.namespace, project) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + end end end - describe 'Find branches' do - it 'shows filtered branches', js: true do + context 'logged out' do + before do visit namespace_project_branches_path(project.namespace, project) + end - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) + it 'does not show merge request button' do + page.within first('.all-branches li') do + expect(page).not_to have_content 'Merge Request' + end end end end diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb index 0cfeb2e57d8..d1685f95503 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'tempfile' describe "Builds" do let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } @@ -6,7 +7,7 @@ describe "Builds" do before do login_as(:user) @commit = FactoryGirl.create :ci_pipeline - @build = FactoryGirl.create :ci_build, pipeline: @commit + @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit @build2 = FactoryGirl.create :ci_build @project = @commit.project @project.team << [@user, :developer] @@ -156,7 +157,6 @@ describe "Builds" do context 'Build raw trace' do before do @build.run! - @build.trace = 'BUILD TRACE' visit namespace_project_build_path(@project.namespace, @project, @build) end @@ -164,6 +164,26 @@ describe "Builds" do expect(page).to have_link 'Raw' end end + + describe 'Variables' do + before do + @trigger_request = create :ci_trigger_request_with_variables + @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it 'shows variable key and value after click', js: true do + expect(page).to have_css('.reveal-variables') + expect(page).not_to have_css('.js-build-variable') + expect(page).not_to have_css('.js-build-value') + + click_button 'Reveal Variables' + + expect(page).not_to have_css('.reveal-variables') + expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') + end + end end describe "POST /:project/builds/:id/cancel" do @@ -255,35 +275,101 @@ describe "Builds" do end end - describe "GET /:project/builds/:id/raw" do - context "Build from project" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - @build.trace = 'BUILD TRACE' - visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.js-build-sidebar') { click_link 'Raw' } + describe 'GET /:project/builds/:id/raw' do + context 'access source' do + context 'build from project' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end end - it 'sends the right headers' do - expect(page.status_code).to eq(200) - expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + context 'build from other project' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build2.run! + visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end end end - context "Build from other project" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build2.run! - @build2.trace = 'BUILD TRACE' - visit raw_namespace_project_build_path(@project.namespace, @project, @build2) - puts page.status_code - puts current_url + context 'storage form' do + let(:existing_file) { Tempfile.new('existing-trace-file').path } + let(:non_existing_file) do + file = Tempfile.new('non-existing-trace-file') + path = file.path + file.unlink + path end - it 'sends the right headers' do - expect(page.status_code).to eq(404) + context 'when build has trace in file' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(existing_file) + end + end + + context 'when build has trace in old file' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(999) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(existing_file) + end + end + + context 'when build has trace in DB' do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + + allow_any_instance_of(Project).to receive(:ci_id).and_return(nil) + allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file) + allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file) + + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end end end end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb new file mode 100644 index 00000000000..a1643fd1f43 --- /dev/null +++ b/spec/features/projects/edit_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +feature 'Project edit', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + login_as(user) + + visit edit_namespace_project_path(project.namespace, project) + end + + context 'feature visibility' do + context 'merge requests select' do + it 'hides merge requests section' do + select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + + expect(page).to have_selector('.merge-requests-feature', visible: false) + end + + it 'hides merge requests section after save' do + select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + + expect(page).to have_selector('.merge-requests-feature', visible: false) + + click_button 'Save changes' + + wait_for_ajax + + expect(page).to have_selector('.merge-requests-feature', visible: false) + end + end + + context 'builds select' do + it 'hides merge requests section' do + select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + + expect(page).to have_selector('.builds-feature', visible: false) + end + + it 'hides merge requests section after save' do + select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + + expect(page).to have_selector('.builds-feature', visible: false) + + click_button 'Save changes' + + wait_for_ajax + + expect(page).to have_selector('.builds-feature', visible: false) + end + end + end +end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb new file mode 100644 index 00000000000..9b487e350f2 --- /dev/null +++ b/spec/features/projects/features_visibility_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' +include WaitForAjax + +describe 'Edit Project Settings', feature: true do + let(:member) { create(:user) } + let!(:project) { create(:project, :public, path: 'gitlab', name: 'sample') } + let(:non_member) { create(:user) } + + describe 'project features visibility selectors', js: true do + before do + project.team << [member, :master] + login_as(member) + end + + tools = { builds: "pipelines", issues: "issues", wiki: "wiki", snippets: "snippets", merge_requests: "merge_requests" } + + tools.each do |tool_name, shortcut_name| + describe "feature #{tool_name}" do + it 'toggles visibility' do + visit edit_namespace_project_path(project.namespace, project) + + select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") + + select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" + click_button 'Save changes' + wait_for_ajax + expect(page).to have_selector(".shortcuts-#{shortcut_name}") + + sleep 0.1 + end + end + end + end + + describe 'project features visibility pages' do + before do + @tools = + { + builds: namespace_project_pipelines_path(project.namespace, project), + issues: namespace_project_issues_path(project.namespace, project), + wiki: namespace_project_wiki_path(project.namespace, project, :home), + snippets: namespace_project_snippets_path(project.namespace, project), + merge_requests: namespace_project_merge_requests_path(project.namespace, project), + } + end + + context 'normal user' do + it 'renders 200 if tool is enabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED) + visit url + expect(page.status_code).to eq(200) + end + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 404 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if users is member of group' do + group = create(:group) + project.group = group + project.save + + group.add_owner(member) + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + end + + context 'admin user' do + before do + non_member.update_attribute(:admin, true) + login_as(non_member) + end + + it 'renders 404 if feature is disabled' do + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) + end + end + + it 'renders 200 if feature is enabled only for team members' do + project.team.truncate + + @tools.each do |method_name, url| + project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) + end + end + 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/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 7bb0d26b21c..e14b2705704 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz 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/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/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_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb new file mode 100644 index 00000000000..83cf306437d --- /dev/null +++ b/spec/features/todos/todos_filtering_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe 'Dashboard > User filters todos', feature: true, js: true do + include WaitForAjax + + let(:user_1) { create(:user, username: 'user_1', name: 'user_1') } + let(:user_2) { create(:user, username: 'user_2', name: 'user_2') } + + let(:project_1) { create(:empty_project, name: 'project_1') } + let(:project_2) { create(:empty_project, name: 'project_2') } + + let(:issue) { create(:issue, title: 'issue', project: project_1) } + + let!(:merge_request) { create(:merge_request, source_project: project_2, title: 'merge_request') } + + before do + create(:todo, user: user_1, author: user_2, project: project_1, target: issue, action: 1) + create(:todo, user: user_1, author: user_1, project: project_2, target: merge_request, action: 2) + + project_1.team << [user_1, :developer] + project_2.team << [user_1, :developer] + login_as(user_1) + visit dashboard_todos_path + end + + it 'filters by project' do + click_button 'Project' + within '.dropdown-menu-project' do + fill_in 'Search projects', with: project_1.name_with_namespace + click_link project_1.name_with_namespace + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content project_2.name_with_namespace + end + + it 'filters by author' do + click_button 'Author' + within '.dropdown-menu-author' do + fill_in 'Search authors', with: user_1.name + click_link user_1.name + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content user_2.name + end + + it 'filters by type' do + click_button 'Type' + within '.dropdown-menu-type' do + click_link 'Issue' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' merge request !' + end + + it 'filters by action' do + click_button 'Action' + within '.dropdown-menu-action' do + click_link 'Assigned' + end + wait_for_ajax + expect('.prepend-top-default').not_to have_content ' mentioned ' + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 0342f4f1d97..fc555a74f30 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 @@ -97,6 +118,20 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_css("#todo_#{Todo.first.id}") end end + + describe 'mark all as done', js: true do + before do + visit dashboard_todos_path + click_link('Mark all as done') + end + + it 'shows "All done" message!' do + within('.todos-pending-count') { expect(page).to have_content '0' } + expect(page).to have_content 'To do 0' + expect(page).to have_content "You're all done!" + expect(page).not_to have_selector('.gl-pagination') + end + end end context 'User has a Todo in a project pending deletion' do diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 3cbc8253ad6..72354834c5a 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -12,7 +12,7 @@ describe 'Triggers' do context 'create a trigger' do before do - click_on 'Add Trigger' + click_on 'Add trigger' expect(@project.triggers.count).to eq(1) end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index a46e48c76ed..ff6933dc8d9 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -156,6 +156,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: describe "when 2FA via OTP is disabled" do it "allows logging in with the U2F device" do + user.update_attribute(:otp_required_for_login, false) login_with(user) @u2f_device.respond_to_u2f_authentication @@ -181,6 +182,19 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: end end + it 'persists remember_me value via hidden field' do + login_with(user, remember: true) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + + within 'div#js-authenticate-u2f' do + field = first('input#user_remember_me', visible: false) + expect(field.value).to eq '1' + end + end + describe "when a given U2F device has already been registered by another user" do describe "but not the current user" do it "does not allow logging in with that particular device" do diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb new file mode 100644 index 00000000000..356a8d668b0 --- /dev/null +++ b/spec/features/users/snippets_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Snippets tab on a user profile', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + + context 'when the user has snippets' do + before do + create_list(:snippet, 25, :public, author: user) + + visit user_path(user) + page.within('.user-profile-nav') { click_link 'Snippets' } + wait_for_ajax + end + + it 'is limited to 20 items per page' do + expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20) + end + + context 'clicking on the link to the second page' do + before { click_link('2') } + + it 'shows the remaining snippets' do + expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5) + end + end + end +end diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb new file mode 100644 index 00000000000..b0811d134fa --- /dev/null +++ b/spec/finders/pipelines_finder_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe PipelinesFinder do + let(:project) { create(:project) } + + let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') } + let!(:branch_pipeline) { create(:ci_pipeline, project: project) } + + subject { described_class.new(project).execute(params) } + + describe "#execute" do + context 'when a scope is passed' do + context 'when scope is nil' do + let(:params) { { scope: nil } } + + it 'selects all pipelines' do + expect(subject.count).to be 2 + expect(subject).to include tag_pipeline + expect(subject).to include branch_pipeline + end + end + + context 'when selecting branches' do + let(:params) { { scope: 'branches' } } + + it 'excludes tags' do + expect(subject).not_to include tag_pipeline + expect(subject).to include branch_pipeline + end + end + + context 'when selecting tags' do + let(:params) { { scope: 'tags' } } + + it 'excludes branches' do + expect(subject).to include tag_pipeline + expect(subject).not_to include branch_pipeline + end + end + end + + # Scoping to running will speed up the test as it doesn't hit the FS + let(:params) { { scope: 'running' } } + + it 'orders in descending order on ID' do + feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature') + + expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse + expect(subject.map(&:id)).to eq expected_ids + end + end +end diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb new file mode 100644 index 00000000000..2ac810e478a --- /dev/null +++ b/spec/finders/tags_finder_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe TagsFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:repository) { project.repository } + + describe '#execute' do + context 'sort only' do + it 'sorts by name' do + tags_finder = described_class.new(repository, {}) + + result = tags_finder.execute + + expect(result.first.name).to eq("v1.0.0") + end + + it 'sorts by recently_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_desc' }) + + result = tags_finder.execute + recently_updated_tag = repository.tags.max do |a, b| + repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + end + + expect(result.first.name).to eq(recently_updated_tag.name) + end + + it 'sorts by last_updated' do + tags_finder = described_class.new(repository, { sort: 'updated_asc' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + end + end + + context 'filter only' do + it 'filters tags by name' do + tags_finder = described_class.new(repository, { search: '1.0.0' }) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(1) + end + + it 'does not find any tags with that name' do + tags_finder = described_class.new(repository, { search: 'hey' }) + + result = tags_finder.execute + + expect(result.count).to eq(0) + end + end + + context 'filter and sort' do + it 'filters tags by name and sorts by recently_updated' do + params = { sort: 'updated_desc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.1.0') + expect(result.count).to eq(2) + end + + it 'filters tags by name and sorts by last_updated' do + params = { sort: 'updated_asc', search: 'v1' } + tags_finder = described_class.new(repository, params) + + result = tags_finder.execute + + expect(result.first.name).to eq('v1.0.0') + expect(result.count).to eq(2) + end + end + end +end diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json index 0d2067f704a..70771b21c96 100644 --- a/spec/fixtures/api/schemas/issues.json +++ b/spec/fixtures/api/schemas/issues.json @@ -1,4 +1,15 @@ { - "type": "array", - "items": { "$ref": "issue.json" } + "type": "object", + "required" : [ + "issues", + "size" + ], + "properties" : { + "issues": { + "type": "array", + "items": { "$ref": "issue.json" } + }, + "size": { "type": "integer" } + }, + "additionalProperties": false } diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb new file mode 100644 index 00000000000..9b1ef1e05a2 --- /dev/null +++ b/spec/helpers/git_helper_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe GitHelper do + describe '#short_sha' do + let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') } + + it { expect(short_sha).to eq('d4e043f6') } + end +end diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 3391234e9f5..187b891b927 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -1,6 +1,30 @@ require 'rails_helper' describe ImportHelper do + describe '#import_project_target' do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'when current user can create namespaces' do + it 'returns project namespace' do + user.update_attribute(:can_create_group, true) + + expect(helper.import_project_target('asd', 'vim')).to eq 'asd/vim' + end + end + + context 'when current user can not create namespaces' do + it "takes the current user's namespace" do + user.update_attribute(:can_create_group, false) + + expect(helper.import_project_target('asd', 'vim')).to eq "#{user.namespace_path}/vim" + end + end + end + describe '#github_project_link' do context 'when provider does not specify a custom URL' do it 'uses default GitHub URL' do diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb deleted file mode 100644 index e4d18d8bfc6..00000000000 --- a/spec/helpers/nav_helper_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -# Specs in this file have access to a helper object that includes -# the NavHelper. For example: -# -# describe NavHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -describe NavHelper do - describe '#nav_menu_collapsed?' do - it 'returns true when the nav is collapsed in the cookie' do - helper.request.cookies[:collapsed_nav] = 'true' - expect(helper.nav_menu_collapsed?).to eq true - end - - it 'returns false when the nav is not collapsed in the cookie' do - helper.request.cookies[:collapsed_nav] = 'false' - expect(helper.nav_menu_collapsed?).to eq false - end - end -end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 284b58d8d5c..70032e7df94 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -174,4 +174,48 @@ describe ProjectsHelper do end end end + + describe "#project_feature_access_select" do + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + context "when project is internal or public" do + it "shows all options" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).to include("Everyone with access") + end + end + + context "when project is private" do + before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + + it "shows only allowed options" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).not_to include("Everyone with access") + end + end + + context "when project moves from public to private" do + before do + project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED) + project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it "shows the highest allowed level selected" do + helper.instance_variable_set(:@project, project) + result = helper.project_feature_access_select(:issues_access_level) + + expect(result).to include("Disabled") + expect(result).to include("Only team members") + expect(result).not_to include("Everyone with access") + expect(result).to have_selector('option[selected]', text: "Only team members") + end + end + end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index b0bb991539b..4b2ca3514f8 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -32,6 +32,10 @@ describe SearchHelper do expect(search_autocomplete_opts("adm").size).to eq(1) end + it "does not allow regular expression in search term" do + expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0) + end + it "includes the user's groups" do create(:group).add_owner(user) expect(search_autocomplete_opts("gro").size).to eq(1) diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb new file mode 100644 index 00000000000..d60839b78ec --- /dev/null +++ b/spec/helpers/sidekiq_helper_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe SidekiqHelper do + describe 'parse_sidekiq_ps' do + it 'parses line with time' do + line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with date' do + line = '55137 10,0 2,1 S+ Aug 4 sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with two digit date' do + line = '55137 10,0 2,1 S+ Aug 04 sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'parses line with dot as float separator' do + line = '55137 10.0 2.1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] ' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]']) + end + + it 'does fail gracefully on line not matching the format' do + line = '55137 10.0 2.1 S+ 2:30pm something' + parts = helper.parse_sidekiq_ps(line) + + expect(parts).to eq(['?', '?', '?', '?', '?', '?']) + end + end +end diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index b48026c3b77..56b98856614 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -13,17 +13,21 @@ gl.utils.preventDisabledButtons(); isClicked = false; $button = $('#test-button'); + expect($button).toExist(); $button.click(function() { return isClicked = true; }); $button.trigger('click'); return expect(isClicked).toBe(false); }); - return it('should be on the same page if a disabled link clicked', function() { - var locationBeforeLinkClick; + + it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick, $link; locationBeforeLinkClick = window.location.href; gl.utils.preventDisabledButtons(); - $('#test-link').click(); + $link = $('#test-link'); + expect($link).toExist(); + $link.click(); return expect(window.location.href).toBe(locationBeforeLinkClick); }); }); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index fa32d0d7da5..019ce3b0702 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,17 +1,11 @@ /*= require awards_handler */ - - /*= require jquery */ - - /*= require jquery.cookie */ - - /*= require ./fixtures/emoji_menu */ (function() { - var awardsHandler, lazyAssert; + var awardsHandler, lazyAssert, urlRoot; awardsHandler = null; @@ -27,11 +21,13 @@ }; gon.award_menu_url = '/emojis'; + urlRoot = gon.relative_url_root; lazyAssert = function(done, assertFn) { return setTimeout(function() { assertFn(); return done(); + // Maybe jasmine.clock here? }, 333); }; @@ -45,9 +41,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 +190,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/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 4c52ecd903d..13babb5bfdb 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -8,6 +8,7 @@ beforeEach(function() { fixture.load('behaviors/quick_submit.html'); $('form').submit(function(e) { + // Prevent a form submit from moving us off the testing page return e.preventDefault(); }); return this.spies = { @@ -38,6 +39,8 @@ expect($('input[type=submit]')).toBeDisabled(); return expect($('button[type=submit]')).toBeDisabled(); }); + // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + // only run the tests that apply to the current platform if (navigator.userAgent.match(/Macintosh/)) { it('responds to Meta+Enter', function() { $('input.quick-submit-input').trigger(keydownEvent()); 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/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 0c37ec8354f..f3797ed44d4 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -26,12 +26,15 @@ const listObjDuplicate = { const BoardsMockData = { 'GET': { - '/test/issue-boards/board/lists{/id}/issues': [{ - title: 'Testing', - iid: 1, - confidential: false, - labels: [] - }] + '/test/issue-boards/board/lists{/id}/issues': { + issues: [{ + title: 'Testing', + iid: 1, + confidential: false, + labels: [] + }], + size: 1 + } }, 'POST': { '/test/issue-boards/board/lists{/id}': listObj diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee deleted file mode 100644 index 6b9617341fe..00000000000 --- a/spec/javascripts/datetime_utility_spec.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -#= require lib/utils/datetime_utility - -describe 'Date time utils', -> - describe 'get day name', -> - it 'should return Sunday', -> - day = gl.utils.getDayName(new Date('07/17/2016')) - expect(day).toBe('Sunday') - - it 'should return Monday', -> - day = gl.utils.getDayName(new Date('07/18/2016')) - expect(day).toBe('Monday') - - it 'should return Tuesday', -> - day = gl.utils.getDayName(new Date('07/19/2016')) - expect(day).toBe('Tuesday') - - it 'should return Wednesday', -> - day = gl.utils.getDayName(new Date('07/20/2016')) - expect(day).toBe('Wednesday') - - it 'should return Thursday', -> - day = gl.utils.getDayName(new Date('07/21/2016')) - expect(day).toBe('Thursday') - - it 'should return Friday', -> - day = gl.utils.getDayName(new Date('07/22/2016')) - expect(day).toBe('Friday') - - it 'should return Saturday', -> - day = gl.utils.getDayName(new Date('07/23/2016')) - expect(day).toBe('Saturday') diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 new file mode 100644 index 00000000000..a2d1b0a7732 --- /dev/null +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -0,0 +1,64 @@ +//= require lib/utils/datetime_utility +(() => { + describe('Date time utils', () => { + describe('get day name', () => { + it('should return Sunday', () => { + const day = gl.utils.getDayName(new Date('07/17/2016')); + expect(day).toBe('Sunday'); + }); + + it('should return Monday', () => { + const day = gl.utils.getDayName(new Date('07/18/2016')); + expect(day).toBe('Monday'); + }); + + it('should return Tuesday', () => { + const day = gl.utils.getDayName(new Date('07/19/2016')); + expect(day).toBe('Tuesday'); + }); + + it('should return Wednesday', () => { + const day = gl.utils.getDayName(new Date('07/20/2016')); + expect(day).toBe('Wednesday'); + }); + + it('should return Thursday', () => { + const day = gl.utils.getDayName(new Date('07/21/2016')); + expect(day).toBe('Thursday'); + }); + + it('should return Friday', () => { + const day = gl.utils.getDayName(new Date('07/22/2016')); + expect(day).toBe('Friday'); + }); + + it('should return Saturday', () => { + const day = gl.utils.getDayName(new Date('07/23/2016')); + expect(day).toBe('Saturday'); + }); + }); + + describe('get day difference', () => { + it('should return 7', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('07/08/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(7); + }); + + it('should return 31', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('08/01/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(31); + }); + + it('should return 365', () => { + const firstDay = new Date('07/02/2015'); + const secondDay = new Date('07/01/2016'); + const difference = gl.utils.getDayDifference(firstDay, secondDay); + expect(difference).toBe(365); + }); + }); + }); +})(); 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/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml new file mode 100644 index 00000000000..cc1f8f15c21 --- /dev/null +++ b/spec/javascripts/fixtures/comments.html.haml @@ -0,0 +1,21 @@ +.flash-container.timeline-content +.timeline-icon.hidden-xs.hidden-sm + %a.author_link + %img +.timeline-content.timeline-content-form + %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form + .md-area + .md-header + .md-write-holder + .zen-backdrop.div-dropzone-wrapper + .div-dropzone-wrapper + .div-dropzone.dz-clickable + %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area + .note-form-actions.clearfix + %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' } + %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen + Reopen issue + %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close + Close issue + %a.btn.btn-cancel.js-note-discard + Discard draft
\ No newline at end of file diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml index 859e79a6c9e..779d6429a5f 100644 --- a/spec/javascripts/fixtures/u2f/authenticate.html.haml +++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml @@ -1 +1 @@ -= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" } += render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" } diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 82ee1954a59..d5401fbb0d1 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -7,7 +7,7 @@ describe("ContributorsGraph", function () { expect(ContributorsGraph.prototype.x_domain).toEqual(20) }) }) - + describe("#set_y_domain", function () { it("sets the y_domain", function () { ContributorsGraph.set_y_domain([{commits: 30}]) @@ -89,7 +89,7 @@ describe("ContributorsGraph", function () { }) describe("ContributorsMasterGraph", function () { - + // TODO: fix or remove //describe("#process_dates", function () { //it("gets and parses dates", function () { @@ -103,7 +103,7 @@ describe("ContributorsMasterGraph", function () { //expect(graph.get_dates).toHaveBeenCalledWith(data) //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") //}) - //}) + //}) describe("#get_dates", function () { it("plucks the date field from data collection", function () { @@ -124,5 +124,5 @@ describe("ContributorsMasterGraph", function () { }) }) - + }) diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index dc6231ebb38..33690c7a5f3 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,7 +1,5 @@ /*= require lib/utils/text_utility */ - - /*= require issue */ (function() { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 25d3f5b6c04..f09596bd36d 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,5 @@ /*= require jquery-ui/autocomplete */ - - /*= require new_branch_form */ (function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 14dc6bfdfde..a588f403dd5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,8 +1,7 @@ - /*= require notes */ - - +/*= require autosize */ /*= require gl_form */ +/*= require lib/utils/text_utility */ (function() { window.gon || (window.gon = {}); @@ -12,29 +11,63 @@ }; describe('Notes', function() { - return describe('task lists', function() { + describe('task lists', function() { fixture.preload('issue_note.html'); + beforeEach(function() { fixture.load('issue_note.html'); $('form').on('submit', function(e) { - return e.preventDefault(); + e.preventDefault(); }); - return this.notes = new Notes(); + this.notes = new Notes(); }); + it('modifies the Markdown field', function() { $('input[type=checkbox]').attr('checked', true).trigger('change'); - return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits the form on tasklist:changed', function() { - var submitted; - submitted = false; + + it('submits the form on tasklist:changed', function() { + var submitted = false; $('form').on('submit', function(e) { submitted = true; - return e.preventDefault(); + e.preventDefault(); }); + $('.js-task-list-field').trigger('tasklist:changed'); - return expect(submitted).toBe(true); + expect(submitted).toBe(true); + }); + }); + + describe('comments', function() { + var commentsTemplate = 'comments.html'; + var textarea = '.js-note-text'; + fixture.preload(commentsTemplate); + + beforeEach(function() { + fixture.load(commentsTemplate); + this.notes = new Notes(); + + this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update'); + spyOn(this.notes, 'renderNote').and.stub(); + + $(textarea).data('autosave', { + reset: function() {} + }); + + $('form').on('submit', function(e) { + e.preventDefault(); + $('.js-main-target-form').trigger('ajax:success'); + }); }); + + it('autosizes after comment submission', function() { + $(textarea).text('This is an example comment note'); + expect(this.autoSizeSpy).not.toHaveBeenTriggered(); + + $('.js-comment-button').click(); + expect(this.autoSizeSpy).toHaveBeenTriggered(); + }) }); }); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index ffe49828492..51eb12b41d4 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,22 +1,10 @@ /*= require bootstrap */ - - /*= require select2 */ - - /*= require lib/utils/type_utility */ - - /*= require gl_dropdown */ - - /*= require api */ - - /*= require project_select */ - - /*= require project */ (function() { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 38b3b2653ec..c937a4706f7 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,10 +1,6 @@ /*= require right_sidebar */ - - /*= require jquery */ - - /*= require jquery.cookie */ (function() { diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 324f5152780..00d9fc1302a 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,19 +1,9 @@ /*= require gl_dropdown */ - - /*= require search_autocomplete */ - - /*= require jquery */ - - /*= require lib/utils/common_utils */ - - /*= require lib/utils/type_utility */ - - /*= require fuzzaldrin-plus */ (function() { @@ -43,6 +33,8 @@ groupName = 'Gitlab Org'; + // Add required attributes to body before starting the test. + // section would be dashboard|group|project addBodyAttributes = function(section) { var $body; if (section == null) { @@ -64,6 +56,7 @@ } }; + // Mock `gl` object in window for dashboard specific page. App code will need it. mockDashboardOptions = function() { window.gl || (window.gl = {}); return window.gl.dashboardOptions = { @@ -72,6 +65,7 @@ }; }; + // Mock `gl` object in window for project specific page. App code will need it. mockProjectOptions = function() { window.gl || (window.gl = {}); return window.gl.projectOptions = { diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7b6b55fe545..04ccf246052 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -10,6 +10,7 @@ }); return describe('#replyWithSelectedText', function() { var stubSelection; + // Stub window.getSelection to return the provided String. stubSelection = function(text) { return window.getSelection = function() { return text; diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 7d91ed0f855..8801c297887 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -1,21 +1,41 @@ - +// PhantomJS (Teaspoons default driver) doesn't have support for +// Function.prototype.bind, which has caused confusion. Use this polyfill to +// avoid the confusion. /*= require support/bind-poly */ - +// You can require your own javascript files here. By default this will include +// everything in application, however you may get better load performance if you +// require the specific files that are being used in the spec that tests them. /*= require jquery */ - - /*= require jquery.turbolinks */ - - /*= require bootstrap */ - - /*= require underscore */ - +// Teaspoon includes some support files, but you can use anything from your own +// support path too. +// require support/jasmine-jquery-1.7.0 +// require support/jasmine-jquery-2.0.0 /*= require support/jasmine-jquery-2.1.0 */ +// require support/sinon +// require support/your-support-file +// Deferring execution +// If you're using CommonJS, RequireJS or some other asynchronous library you can +// defer execution. Call Teaspoon.execute() after everything has been loaded. +// Simple example of a timeout: +// Teaspoon.defer = true +// setTimeout(Teaspoon.execute, 1000) +// Matching files +// By default Teaspoon will look for files that match +// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +// and it'll be included in the default suite automatically. If you want to +// customize suites, check out the configuration in teaspoon_env.rb +// Manifest +// If you'd rather require your spec files manually (to control order for +// instance) you can disable the suite matcher in the configuration and use this +// file as a manifest. +// For more information: http://github.com/modeset/teaspoon + (function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index e008ce956ad..7ce3884f844 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,16 +1,8 @@ /*= require u2f/authenticate */ - - /*= require u2f/util */ - - /*= require u2f/error */ - - /*= require u2f */ - - /*= require ./mock_u2f_device */ (function() { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 21c5266c60e..01d6b7a8961 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,16 +1,8 @@ /*= require u2f/register */ - - /*= require u2f/util */ - - /*= require u2f/error */ - - /*= require u2f */ - - /*= require ./mock_u2f_device */ (function() { diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 3d680ec8ea3..0c1266800d7 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -14,8 +14,10 @@ return true; } }; + // Stub Dropzone.forElement(...).enable() }); this.zen = new ZenMode(); + // Set this manually because we can't actually scroll the window return this.zen.scroll_position = 456; }); describe('on enter', function() { @@ -60,7 +62,7 @@ return $('a.js-zen-enter').click(); }; - exitZen = function() { + exitZen = function() { // Ohmmmmmmm return $('a.js-zen-leave').click(); }; diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index 593bd6d5cac..e6c90ad87ee 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -65,14 +65,14 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - it 'includes a title attribute' do + it 'includes no title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq range.reference_title + expect(doc.css('a').first.attr('title')).to eq "" end it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index d46d3f1489e..e0f08282551 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -55,7 +55,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq commit.link_title + expect(doc.css('a').first.attr('title')).to eq commit.title end it 'escapes the title attribute' do @@ -67,7 +67,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("See #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 953466679e4..7116c09fb21 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -64,7 +64,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'includes default classes' do doc = filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index a005b4990e7..fce86a9b6ad 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -54,7 +54,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + expect(doc.css('a').first.attr('title')).to eq issue.title end it 'escapes the title attribute' do @@ -66,7 +66,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Issue #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9276a154007..908ccebbf87 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 805acf1c8b3..274258a045c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -46,7 +46,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + expect(doc.css('a').first.attr('title')).to eq merge.title end it 'escapes the title attribute' do @@ -58,7 +58,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 9424f2363e1..7419863d848 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -20,7 +20,7 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 5068ddd7faa..9b92d1a3926 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -39,7 +39,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes a title attribute' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + expect(doc.css('a').first.attr('title')).to eq snippet.title end it 'escapes the title attribute' do @@ -51,7 +51,7 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Snippet #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet has-tooltip' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 108b36a97cc..fdbdb21eac1 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -104,7 +104,7 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do it 'includes default classes' do doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip' end it 'supports an :only_path context' do diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index ac9c66e2663..9095d2b1345 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -39,7 +39,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -57,7 +57,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) @@ -221,7 +221,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'delegates the permissions check to the Ability class' do user = double(:user) - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, project) subject.can?(user, :read_project, project) diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 9a82891297d..4e7f82a6e09 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -82,7 +82,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns the nodes if the user can read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(true) @@ -90,7 +90,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do end it 'returns an empty Array if the user can not read the group' do - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_group, group). and_return(false) @@ -103,7 +103,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s - expect(Ability.abilities).not_to receive(:allowed?) + expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end @@ -113,7 +113,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(true) @@ -125,7 +125,7 @@ describe Banzai::ReferenceParser::UserParser, lib: true do link['data-project'] = other_project.id.to_s - expect(Ability.abilities).to receive(:allowed?). + expect(Ability).to receive(:allowed?). with(user, :read_project, other_project). and_return(false) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index be51d942af7..af192664b33 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1250,5 +1250,40 @@ EOT end end end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(GitlabCiYamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(GitlabCiYamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index b0772cad312..7c23e02d05a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -7,7 +7,8 @@ describe Gitlab::Auth, lib: true do it 'recognizes CI' do token = '123' project = create(:empty_project) - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + ip = 'ip' expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb index 50f619ce26e..e251210949c 100644 --- a/spec/lib/gitlab/ci/config/node/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.process! } + before { entry.compose! } context 'when entry config value is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index d26185ba585..a699089c563 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do .value(nil) .create! - expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined + expect(entry) + .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index 2f87d270b36..12232ff7e2f 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when hash is valid' do - context 'when all entries defined' do + context 'when some entries defined' do let(:hash) do { before_script: ['ls', 'pwd'], image: 'ruby:2.2', @@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do stages: ['build', 'pages'], cache: { key: 'k', untracked: true, paths: ['public/'] }, rspec: { script: %w[rspec ls] }, - spinach: { script: 'spinach' } } + spinach: { before_script: [], variables: {}, script: 'spinach' } } end - describe '#process!' do - before { global.process! } + describe '#compose!' do + before { global.compose! } it 'creates nodes hash' do expect(global.descendants).to be_an Array @@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do end end - context 'when not processed' do + context 'when not composed' do describe '#before_script' do it 'returns nil' do expect(global.before_script).to be nil @@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do end end - context 'when processed' do - before { global.process! } + context 'when composed' do + before { global.compose! } + + describe '#errors' do + it 'has no errors' do + expect(global.errors).to be_empty + end + end describe '#before_script' do it 'returns correct script' do @@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.jobs).to eq( rspec: { name: :rspec, script: %w[rspec ls], - stage: 'test' }, + before_script: ['ls', 'pwd'], + commands: "ls\npwd\nrspec\nls", + image: 'ruby:2.2', + services: ['postgres:9.1', 'mysql:5.5'], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'] }, + variables: { VAR: 'value' }, + after_script: ['make clean'] }, spinach: { name: :spinach, + before_script: [], script: %w[spinach], - stage: 'test' } + commands: 'spinach', + image: 'ruby:2.2', + services: ['postgres:9.1', 'mysql:5.5'], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'] }, + variables: {}, + after_script: ['make clean'] }, ) end end @@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when most of entires not defined' do - let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } - before { global.process! } + before { global.compose! } + + let(:hash) do + { cache: { key: 'a' }, rspec: { script: %w[ls] } } + end describe '#nodes' do it 'instantizes all nodes' do expect(global.descendants.count).to eq 8 end - it 'contains undefined nodes' do + it 'contains unspecified nodes' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined + .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified end end @@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do # details. # context 'when entires specified but not defined' do - let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } - before { global.process! } + before { global.compose! } + + let(:hash) do + { variables: nil, rspec: { script: 'rspec' } } + end describe '#variables' do it 'undefined entry returns a default value' do @@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when hash is not valid' do - before { global.process! } + before { global.compose! } let(:hash) do { before_script: 'ls' } @@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.specified?).to be true end end + + describe '#[]' do + before { global.compose! } + + let(:hash) do + { cache: { key: 'a' }, rspec: { script: 'ls' } } + end + + context 'when node exists' do + it 'returns correct entry' do + expect(global[:cache]) + .to be_an_instance_of Gitlab::Ci::Config::Node::Cache + expect(global[:jobs][:rspec][:script].value).to eq ['ls'] + end + end + + context 'when node does not exist' do + it 'always return unspecified node' do + expect(global[:some][:unknown][:node]) + .not_to be_specified + end + 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/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb index 1484fb60dd8..91f676dae03 100644 --- a/spec/lib/gitlab/ci/config/node/job_spec.rb +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Job do let(:entry) { described_class.new(config, name: :rspec) } - before { entry.process! } - describe 'validations' do + before { entry.compose! } + context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } @@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do end end - describe '#value' do - context 'when entry is correct' do + describe '#relevant?' do + it 'is a relevant entry' do + expect(entry).to be_relevant + end + end + + describe '#compose!' do + let(:unspecified) { double('unspecified', 'specified?' => false) } + + let(:specified) do + double('specified', 'specified?' => true, value: 'specified') + end + + let(:deps) { double('deps', '[]' => unspecified) } + + context 'when job config overrides global config' do + before { entry.compose!(deps) } + let(:config) do - { before_script: %w[ls pwd], - script: 'rspec', - after_script: %w[cleanup] } + { image: 'some_image', cache: { key: 'test' } } + end + + it 'overrides global config' do + expect(entry[:image].value).to eq 'some_image' + expect(entry[:cache].value).to eq(key: 'test') + end + end + + context 'when job config does not override global config' do + before do + allow(deps).to receive('[]').with(:image).and_return(specified) + entry.compose!(deps) end - it 'returns correct value' do - expect(entry.value) - .to eq(name: :rspec, - before_script: %w[ls pwd], - script: %w[rspec], - stage: 'test', - after_script: %w[cleanup]) + let(:config) { { script: 'ls', cache: { key: 'test' } } } + + it 'uses config from global entry' do + expect(entry[:image].value).to eq 'specified' + expect(entry[:cache].value).to eq(key: 'test') end end end - describe '#relevant?' do - it 'is a relevant entry' do - expect(entry).to be_relevant + context 'when composed' do + before { entry.compose! } + + describe '#value' do + before { entry.compose! } + + context 'when entry is correct' do + let(:config) do + { before_script: %w[ls pwd], + script: 'rspec', + after_script: %w[cleanup] } + end + + it 'returns correct value' do + expect(entry.value) + .to eq(name: :rspec, + before_script: %w[ls pwd], + script: %w[rspec], + commands: "ls\npwd\nrspec", + stage: 'test', + after_script: %w[cleanup]) + end + end + end + + describe '#commands' do + let(:config) do + { before_script: %w[ls pwd], script: 'rspec' } + end + + it 'returns a string of commands concatenated with new line character' do + expect(entry.commands).to eq "ls\npwd\nrspec" + end end end end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb index b8d9c70479c..929809339ef 100644 --- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.process! } + before { entry.compose! } context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do end end - context 'when valid job entries processed' do - before { entry.process! } + context 'when valid job entries composed' do + before { entry.compose! } let(:config) do { rspec: { script: 'rspec' }, @@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do expect(entry.value).to eq( rspec: { name: :rspec, script: %w[rspec], + commands: 'rspec', stage: 'test' }, spinach: { name: :spinach, script: %w[spinach], + commands: 'spinach', stage: 'test' }) end end @@ -74,7 +76,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/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb deleted file mode 100644 index 1ab5478dcfa..00000000000 --- a/spec/lib/gitlab/ci/config/node/null_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Config::Node::Null do - let(:null) { described_class.new(nil) } - - describe '#leaf?' do - it 'is leaf node' do - expect(null).to be_leaf - end - end - - describe '#valid?' do - it 'is always valid' do - expect(null).to be_valid - end - end - - describe '#errors' do - it 'is does not contain errors' do - expect(null.errors).to be_empty - end - end - - describe '#value' do - it 'returns nil' do - expect(null.value).to eq nil - end - end - - describe '#relevant?' do - it 'is not relevant' do - expect(null.relevant?).to eq false - end - end - - describe '#specified?' do - it 'is not defined' do - expect(null.specified?).to eq false - end - end -end diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb index ee7395362a9..219a7e981d3 100644 --- a/spec/lib/gitlab/ci/config/node/script_spec.rb +++ b/spec/lib/gitlab/ci/config/node/script_spec.rb @@ -3,9 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Script do let(:entry) { described_class.new(config) } - describe '#process!' do - before { entry.process! } - + describe 'validations' do context 'when entry config value is correct' do let(:config) { ['ls', 'pwd'] } diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 2d43e1c1a9d..6bde8602963 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -1,32 +1,41 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Undefined do - let(:undefined) { described_class.new(entry) } - let(:entry) { spy('Entry') } + let(:entry) { described_class.new } + + describe '#leaf?' do + it 'is leaf node' do + expect(entry).to be_leaf + end + end describe '#valid?' do - it 'delegates method to entry' do - expect(undefined.valid).to eq entry + it 'is always valid' do + expect(entry).to be_valid end end describe '#errors' do - it 'delegates method to entry' do - expect(undefined.errors).to eq entry + it 'is does not contain errors' do + expect(entry.errors).to be_empty end end describe '#value' do - it 'delegates method to entry' do - expect(undefined.value).to eq entry + it 'returns nil' do + expect(entry.value).to eq nil end end - describe '#specified?' do - it 'is always false' do - allow(entry).to receive(:specified?).and_return(true) + describe '#relevant?' do + it 'is not relevant' do + expect(entry.relevant?).to eq false + end + end - expect(undefined.specified?).to be false + describe '#specified?' do + it 'is not defined' do + expect(entry.specified?).to eq false end end end diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb new file mode 100644 index 00000000000..ba3ceef24ce --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Unspecified do + let(:unspecified) { described_class.new(entry) } + let(:entry) { spy('Entry') } + + describe '#valid?' do + it 'delegates method to entry' do + expect(unspecified.valid?).to eq entry + end + end + + describe '#errors' do + it 'delegates method to entry' do + expect(unspecified.errors).to eq entry + end + end + + describe '#value' do + it 'delegates method to entry' do + expect(unspecified.value).to eq entry + end + end + + describe '#specified?' do + it 'is always false' do + allow(entry).to receive(:specified?).and_return(true) + + expect(unspecified.specified?).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb new file mode 100644 index 00000000000..b26728a843c --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline_duration_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Gitlab::Ci::PipelineDuration do + let(:calculated_duration) { calculate(data) } + + shared_examples 'calculating duration' do + it do + expect(calculated_duration).to eq(duration) + end + end + + context 'test sample A' do + let(:data) do + [[0, 1], + [1, 2], + [3, 4], + [5, 6]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample B' do + let(:data) do + [[0, 1], + [1, 2], + [2, 3], + [3, 4], + [0, 4]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample C' do + let(:data) do + [[0, 4], + [2, 6], + [5, 7], + [8, 9]] + end + + let(:duration) { 8 } + + it_behaves_like 'calculating duration' + end + + context 'test sample D' do + let(:data) do + [[0, 1], + [2, 3], + [4, 5], + [6, 7]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + context 'test sample E' do + let(:data) do + [[0, 1], + [3, 9], + [3, 4], + [3, 5], + [3, 8], + [4, 5], + [4, 7], + [5, 8]] + end + + let(:duration) { 7 } + + it_behaves_like 'calculating duration' + end + + context 'test sample F' do + let(:data) do + [[1, 3], + [2, 4], + [2, 4], + [2, 4], + [5, 8]] + end + + let(:duration) { 6 } + + it_behaves_like 'calculating duration' + end + + context 'test sample G' do + let(:data) do + [[1, 3], + [2, 4], + [6, 7]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end + + def calculate(data) + periods = data.shuffle.map do |(first, last)| + Gitlab::Ci::PipelineDuration::Period.new(first, last) + end + + Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first)) + end +end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb index 65a828accde..16eb3766356 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/conflict/parser_spec.rb @@ -179,10 +179,15 @@ CONFLICT to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) end - it 'raises UnmergeableFile when the file is over 100 KB' do - expect { parse_text('a' * 102401) }. + it 'raises UnmergeableFile when the file is over 200 KB' do + expect { parse_text('a' * 204801) }. 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/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index b7c3bc4e1a7..7df288f619f 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -3,7 +3,7 @@ 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(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } 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') } @@ -13,7 +13,7 @@ describe Gitlab::GithubImport::Importer, lib: true do 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 + let(:label1) do double( name: 'Bug', color: 'ff0000', @@ -21,6 +21,14 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' + ) + end + let(:milestone) do double( number: 1347, @@ -93,7 +101,7 @@ describe Gitlab::GithubImport::Importer, lib: true do 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(:labels).and_return([label1, label2]) 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]) @@ -113,7 +121,7 @@ describe Gitlab::GithubImport::Importer, lib: true 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: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" }, { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/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)" }, diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 0e7ffbe9b8e..d60c4111e99 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -48,8 +48,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when issue is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -62,7 +61,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(issue.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb index 87593e32db0..8098754d735 100644 --- a/spec/lib/gitlab/github_import/label_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb @@ -1,18 +1,34 @@ require 'spec_helper' describe Gitlab::GithubImport::LabelFormatter, lib: true do - describe '#attributes' do - it 'returns formatted attributes' do - project = create(:project) - raw = double(name: 'improvements', color: 'e6e6e6') + let(:project) { create(:project) } + let(:raw) { double(name: 'improvements', color: 'e6e6e6') } - formatter = described_class.new(project, raw) + subject { described_class.new(project, raw) } - expect(formatter.attributes).to eq({ + describe '#attributes' do + it 'returns formatted attributes' do + expect(subject.attributes).to eq({ project: project, title: 'improvements', color: '#e6e6e6' }) end end + + describe '#create!' do + context 'when label does not exist' do + it 'creates a new label' do + expect { subject.create! }.to change(Label, :count).by(1) + end + end + + context 'when label exists' do + it 'does not create a new label' do + project.labels.create(name: raw.name) + + expect { subject.create! }.not_to change(Label, :count) + end + end + end end diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 5a421e50581..09337c99a07 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -40,8 +40,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -52,7 +51,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do state: 'closed', due_date: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(formatter.attributes).to eq(expected) diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb index 0f363b8b0aa..014ee462e5c 100644 --- a/spec/lib/gitlab/github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -2,33 +2,59 @@ require 'spec_helper' describe Gitlab::GithubImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:namespace) { create(:group, owner: user) } + let(:repo) do OpenStruct.new( login: 'vim', name: 'vim', - private: true, full_name: 'asd/vim', - clone_url: "https://gitlab.com/asd/vim.git", - owner: OpenStruct.new(login: "john") + clone_url: 'https://gitlab.com/asd/vim.git' ) end - let(:namespace) { create(:group, owner: user) } - let(:token) { "asdffg" } - let(:access_params) { { github_access_token: token } } + + subject(:service) { described_class.new(repo, namespace, user, github_access_token: 'asdffg') } before do namespace.add_owner(user) + allow_any_instance_of(Project).to receive(:add_import_job) end - it 'creates project' do - allow_any_instance_of(Project).to receive(:add_import_job) + describe '#execute' do + it 'creates a project' do + expect { service.execute }.to change(Project, :count).by(1) + end + + it 'handle GitHub credentials' do + project = service.execute + + expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git') + expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git') + expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil) + end + + context 'when Github project is private' do + it 'sets project visibility to private' do + repo.private = true + + project = service.execute + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end + + context 'when Github project is public' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets project visibility to the default project visibility' do + repo.private = false - project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params) - project = project_creator.execute + project = service.execute - expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") - expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git") - expect(project.import_data.credentials).to eq(user: "asdffg", password: nil) - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + 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 b667abf063d..edfc6ad81c6 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -62,8 +62,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when pull request is closed' do - let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(state: 'closed', closed_at: closed_at)) } + let(:raw_data) { double(base_data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -81,7 +80,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: closed_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) @@ -108,7 +107,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do author_id: project.creator_id, assignee_id: nil, created_at: created_at, - updated_at: merged_at + updated_at: updated_at } expect(pull_request.attributes).to eq(expected) diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index d3f1deb3837..9b499b593d3 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do 'title' => 'Issue', 'description' => 'Lorem ipsum', 'state' => 'opened', + 'confidential' => true, 'author' => { 'id' => 283999, 'name' => 'John Doe' @@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do title: 'Issue', description: "*Created by: John Doe*\n\nLorem ipsum", state: 'opened', + confidential: true, author_id: project.creator_id } 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/import_export/project.json b/spec/lib/gitlab/import_export/project.json index cbbf98dca94..5114f9c55e1 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -1,9 +1,5 @@ { "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", - "issues_enabled": true, - "merge_requests_enabled": true, - "wiki_enabled": true, - "snippets_enabled": false, "visibility_level": 10, "archived": false, "issues": [ @@ -7307,4 +7303,4 @@ "protected_branches": [ ] -}
\ No newline at end of file +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 4d857945fde..a07ef279e68 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let(:project) { create(:empty_project, name: 'project', path: 'project') } + let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } @@ -18,6 +18,17 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(restored_project_json).to be true end + it 'restore correct project features' do + restored_project_json + project = Project.find_by_path('project') + + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) + end + it 'creates a valid pipeline note' do restored_project_json diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 3a86a4ce07c..d891c2d0cc6 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -111,6 +111,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty end + it 'has project feature' do + project_feature = saved_project_json['project_feature'] + expect(project_feature).not_to be_empty + expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED) + expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED) + expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE) + end + it 'does not complain about non UTF-8 characters in MR diffs' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -154,6 +162,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE) + project end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index b6dec41d218..3ceb1e7e803 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::ImportExport::Reader, lib: true do expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) end + it 'generates the correct hash for a single project feature relation' do + setup_yaml(project_tree: [:project_feature]) + + expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature]) + end + it 'generates the correct hash for a multiple project relation' do setup_yaml(project_tree: [:issues, :snippets]) diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 4847b5f3b0e..0600893f4cf 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -1,12 +1,77 @@ require 'spec_helper' describe Gitlab::LDAP::Adapter, lib: true do - let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' } + include LdapHelpers + + let(:ldap) { double(:ldap) } + let(:adapter) { ldap_adapter('ldapmain', ldap) } + + describe '#users' do + before do + stub_ldap_config(base: 'dc=example,dc=com') + end + + it 'searches with the proper options when searching by uid' do + # Requires this expectation style to match the filter + expect(adapter).to receive(:ldap_search) do |arg| + expect(arg[:filter].to_s).to eq('(uid=johndoe)') + expect(arg[:base]).to eq('dc=example,dc=com') + expect(arg[:attributes]).to match(%w{uid cn mail dn}) + end.and_return({}) + + adapter.users('uid', 'johndoe') + end + + it 'searches with the proper options when searching by dn' do + expect(adapter).to receive(:ldap_search).with( + base: 'uid=johndoe,ou=users,dc=example,dc=com', + scope: Net::LDAP::SearchScope_BaseObject, + attributes: %w{uid cn mail dn}, + filter: nil + ).and_return({}) + + adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com') + end + + it 'searches with the proper options when searching with a limit' do + expect(adapter) + .to receive(:ldap_search).with(hash_including(size: 100)).and_return({}) + + adapter.users('uid', 'johndoe', 100) + end + + it 'returns an LDAP::Person if search returns a result' do + entry = ldap_user_entry('johndoe') + allow(adapter).to receive(:ldap_search).and_return([entry]) + + results = adapter.users('uid', 'johndoe') + + expect(results.size).to eq(1) + expect(results.first.uid).to eq('johndoe') + end + + it 'returns empty array if search entry does not respond to uid' do + entry = Net::LDAP::Entry.new + entry['dn'] = user_dn('johndoe') + allow(adapter).to receive(:ldap_search).and_return([entry]) + + results = adapter.users('uid', 'johndoe') + + expect(results).to be_empty + end + + it 'uses the right uid attribute when non-default' do + stub_ldap_config(uid: 'sAMAccountName') + expect(adapter).to receive(:ldap_search).with( + hash_including(attributes: %w{sAMAccountName cn mail dn}) + ).and_return({}) + + adapter.users('sAMAccountName', 'johndoe') + end + end describe '#dn_matches_filter?' do - let(:ldap) { double(:ldap) } subject { adapter.dn_matches_filter?(:dn, :filter) } - before { allow(adapter).to receive(:ldap).and_return(ldap) } context "when the search is successful" do context "and the result is non-empty" do 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/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index e8b236426e9..4ae216d55b0 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -40,4 +40,13 @@ describe 'Gitlab::Popen', lib: true, no_db: true do it { expect(@status).to be_zero } it { expect(@output).to include('spec') } end + + context 'use stdin' do + before do + @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' } + end + + it { expect(@status).to be_zero } + it { expect(@output).to eq('hello') } + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 8a656ab0ee9..dfbefad6367 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,12 +12,6 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#total_count' do - it 'returns the total amount of search hits' do - expect(results.total_count).to eq(4) - end - end - describe '#projects_count' do it 'returns the total amount of projects' do expect(results.projects_count).to eq(1) @@ -42,18 +36,6 @@ describe Gitlab::SearchResults do end end - describe '#empty?' do - it 'returns true when there are no search results' do - allow(results).to receive(:total_count).and_return(0) - - expect(results.empty?).to eq(true) - end - - it 'returns false when there are search results' do - expect(results.empty?).to eq(false) - end - end - describe 'confidential issues' do let(:project_1) { create(:empty_project) } let(:project_2) { create(:empty_project) } diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index e86b9ef6a63..b661a894c0c 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -5,12 +5,6 @@ describe Gitlab::SnippetSearchResults do let(:results) { described_class.new(Snippet.all, 'foo') } - describe '#total_count' do - it 'returns the total amount of search hits' do - expect(results.total_count).to eq(2) - end - end - describe '#snippet_titles_count' do it 'returns the amount of matched snippet titles' do expect(results.snippet_titles_count).to eq(1) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index c5c1402e8fc..395192149a9 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Workhorse, lib: true do let(:project) { create(:project) } let(:subject) { Gitlab::Workhorse } - describe "#send_git_archive" do + describe ".send_git_archive" do context "when the repository doesn't have an archive file path" do before do allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) @@ -15,4 +15,88 @@ describe Gitlab::Workhorse, lib: true do end end end + + describe ".secret" do + subject { described_class.secret } + + before do + described_class.instance_variable_set(:@secret, nil) + described_class.write_secret + end + + it 'returns 32 bytes' do + expect(subject).to be_a(String) + expect(subject.length).to eq(32) + expect(subject.encoding).to eq(Encoding::ASCII_8BIT) + end + + it 'raises an exception if the secret file cannot be read' do + File.delete(described_class.secret_path) + expect { subject }.to raise_exception(Errno::ENOENT) + end + + it 'raises an exception if the secret file contains the wrong number of bytes' do + File.truncate(described_class.secret_path, 0) + expect { subject }.to raise_exception(RuntimeError) + end + end + + describe ".write_secret" do + let(:secret_path) { described_class.secret_path } + before do + begin + File.delete(secret_path) + rescue Errno::ENOENT + end + + described_class.write_secret + end + + it 'uses mode 0600' do + expect(File.stat(secret_path).mode & 0777).to eq(0600) + end + + it 'writes base64 data' do + bytes = Base64.strict_decode64(File.read(secret_path)) + expect(bytes).not_to be_empty + end + end + + describe '#verify_api_request!' do + let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER } + let(:payload) { { 'iss' => 'gitlab-workhorse' } } + + it 'accepts a correct header' do + headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') } + expect { call_verify(headers) }.not_to raise_error + end + + it 'raises an error when the header is not set' do + expect { call_verify({}) }.to raise_jwt_error + end + + it 'raises an error when the header is not signed' do + headers = { header_key => JWT.encode(payload, nil, 'none') } + expect { call_verify(headers) }.to raise_jwt_error + end + + it 'raises an error when the header is signed with the wrong key' do + headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') } + expect { call_verify(headers) }.to raise_jwt_error + end + + it 'raises an error when the issuer is incorrect' do + payload['iss'] = 'somebody else' + headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') } + expect { call_verify(headers) }.to raise_jwt_error + end + + def raise_jwt_error + raise_error(JWT::DecodeError) + end + + def call_verify(headers) + described_class.verify_api_request!(headers) + end + end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index aa3b2bbf471..1bdf005c823 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -171,70 +171,6 @@ 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 @@ -282,4 +218,17 @@ describe Ability, lib: true do end end end + + describe '.project_disabled_features_rules' do + let(:project) { create(:project, wiki_access_level: ProjectFeature::DISABLED) } + + subject { described_class.allowed(project.owner, project) } + + context 'wiki named abilities' do + it 'disables wiki abilities if the project has no wiki' do + expect(project).to receive(:has_external_wiki?).and_return(false) + expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki) + end + end + end end 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/build_spec.rb b/spec/models/ci/build_spec.rb index 36d10636ae9..bce18b4e99e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -19,4 +19,64 @@ describe Ci::Build, models: true do expect(build.trace).to eq(test_trace) end end + + describe '#has_trace_file?' do + context 'when there is no trace' do + it { expect(build.has_trace_file?).to be_falsey } + it { expect(build.trace).to be_nil } + end + + context 'when there is a trace' do + context 'when trace is stored in file' do + let(:build_with_trace) { create(:ci_build, :trace) } + + it { expect(build_with_trace.has_trace_file?).to be_truthy } + it { expect(build_with_trace.trace).to eq('BUILD TRACE') } + end + + context 'when trace is stored in old file' do + before do + allow(build.project).to receive(:ci_id).and_return(999) + allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false) + allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true) + allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace) + end + + it { expect(build.has_trace_file?).to be_truthy } + it { expect(build.trace).to eq(test_trace) } + end + + context 'when trace is stored in DB' do + before do + allow(build.project).to receive(:ci_id).and_return(nil) + allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace) + allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false) + allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false) + end + + it { expect(build.has_trace_file?).to be_falsey } + it { expect(build.trace).to eq(test_trace) } + end + end + end + + describe '#trace_file_path' do + context 'when trace is stored in file' do + before do + allow(build).to receive(:has_trace_file?).and_return(true) + allow(build).to receive(:has_old_trace_file?).and_return(false) + end + + it { expect(build.trace_file_path).to eq(build.path_to_trace) } + end + + context 'when trace is stored in old file' do + before do + allow(build).to receive(:has_trace_file?).and_return(true) + allow(build).to receive(:has_old_trace_file?).and_return(true) + end + + it { expect(build.trace_file_path).to eq(build.old_path_to_trace) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 721b20e0cb2..fbf945c757c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -124,21 +124,38 @@ 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 } + let(:build) { create_build('build1', current, 10) } + let(:build_b) { create_build('build2', current, 20) } + let(:build_c) { create_build('build3', current + 50, 10) } describe '#duration' do before do - travel_to(current - 120) do + pipeline.update(created_at: current) + + travel_to(current + 5) do pipeline.run + pipeline.save + end + + travel_to(current + 30) do + build.success end - travel_to(current) do - pipeline.succeed + travel_to(current + 40) do + build_b.drop end + + travel_to(current + 70) do + build_c.success + end + + pipeline.drop end it 'matches sum of builds duration' do - expect(pipeline.reload.duration).to eq(120) + pipeline.reload + + expect(pipeline.duration).to eq(40) end end @@ -169,6 +186,14 @@ describe Ci::Pipeline, models: true do expect(pipeline.reload.finished_at).to be_nil end end + + def create_build(name, queued_at = current, started_from = 0) + create(:ci_build, + name: name, + pipeline: pipeline, + queued_at: queued_at, + started_at: queued_at + started_from) + end end describe '#branch?' do @@ -195,6 +220,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/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 384a38ebc69..c41359b55a3 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -76,16 +76,6 @@ describe CommitRange, models: true do end end - describe '#reference_title' do - it 'returns the correct String for three-dot ranges' do - expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}" - end - - it 'returns the correct String for two-dot ranges' do - expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}" - end - end - describe '#to_param' do it 'includes the correct keys' do expect(range.to_param.keys).to eq %i(from to) diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index a371c4a18a9..de791abdf3d 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -45,4 +45,14 @@ describe Issue, "Awardable" do expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) end end + + describe 'querying award_emoji on an Awardable' do + let(:issue) { create(:issue) } + + it 'sorts in ascending fashion' do + create_list(:award_emoji, 3, awardable: issue) + + expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id) + end + end end 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/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb new file mode 100644 index 00000000000..5363aea4d22 --- /dev/null +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectFeaturesCompatibility do + let(:project) { create(:project) } + let(:features) { %w(issues wiki builds merge_requests snippets) } + + # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table + # All those fields got moved to a new table called project_feature and are now integers instead of booleans + # This spec tests if the described concern makes sure parameters received by the API are correctly parsed to the new table + # So we can keep it compatible + + it "converts fields from 'true' to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "true") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from 'false' to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, "false") + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end +end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 6a640474cfe..3db5937a4f3 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -31,6 +31,43 @@ describe DiffNote, models: true do subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + describe ".resolve!" do + let(:current_user) { create(:user) } + let!(:commit_note) { create(:diff_note_on_commit) } + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + let!(:unresolved_note) { create(:diff_note_on_merge_request) } + + before do + described_class.resolve!(current_user) + + commit_note.reload + resolved_note.reload + unresolved_note.reload + end + + it 'resolves only the resolvable, not yet resolved notes' do + expect(commit_note.resolved_at).to be_nil + expect(resolved_note.resolved_by).not_to eq(current_user) + expect(unresolved_note.resolved_at).not_to be_nil + expect(unresolved_note.resolved_by).to eq(current_user) + end + end + + describe ".unresolve!" do + let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) } + + before do + described_class.unresolve! + + resolved_note.reload + end + + it 'unresolves the resolved notes' do + expect(resolved_note.resolved_by).to be_nil + expect(resolved_note.resolved_at).to be_nil + end + end + describe "#position=" do context "when provided a string" do it "sets the position" do diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 179f2e73662..0142706d140 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -238,27 +238,19 @@ describe Discussion, model: true do context "when resolvable" do let(:user) { create(:user) } + let(:second_note) { create(:diff_note_on_commit) } # unresolvable before do allow(subject).to receive(:resolvable?).and_return(true) - - allow(first_note).to receive(:resolvable?).and_return(true) - allow(second_note).to receive(:resolvable?).and_return(false) - allow(third_note).to receive(:resolvable?).and_return(true) end context "when all resolvable notes are resolved" do before do first_note.resolve!(user) third_note.resolve!(user) - end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) + first_note.reload + third_note.reload end it "doesn't change resolved_at on the resolved notes" do @@ -309,46 +301,44 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "doesn't change resolved_at on the resolved note" do expect(first_note.resolved_at).not_to be_nil - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload.resolved_at } end it "doesn't change resolved_by on the resolved note" do expect(first_note.resolved_by).to eq(user) - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved_by } end it "doesn't change the resolved state on the resolved note" do expect(first_note.resolved?).to be true - expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? } + expect { subject.resolve!(current_user) }. + not_to change { first_note.reload && first_note.resolved? } end it "sets resolved_at on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_at).not_to be_nil end it "sets resolved_by on the unresolved note" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved_by).to eq(current_user) end it "marks the unresolved note as resolved" do subject.resolve!(current_user) + third_note.reload expect(third_note.resolved?).to be true end @@ -373,16 +363,10 @@ describe Discussion, model: true do end context "when no resolvable notes are resolved" do - it "calls resolve! on every resolvable note" do - expect(first_note).to receive(:resolve!).with(current_user) - expect(second_note).not_to receive(:resolve!) - expect(third_note).to receive(:resolve!).with(current_user) - - subject.resolve!(current_user) - end - it "sets resolved_at on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_at).not_to be_nil expect(third_note.resolved_at).not_to be_nil @@ -390,6 +374,8 @@ describe Discussion, model: true do it "sets resolved_by on the unresolved notes" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved_by).to eq(current_user) expect(third_note.resolved_by).to eq(current_user) @@ -397,6 +383,8 @@ describe Discussion, model: true do it "marks the unresolved notes as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(first_note.resolved?).to be true expect(third_note.resolved?).to be true @@ -404,18 +392,24 @@ describe Discussion, model: true do it "sets resolved_at" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_at).not_to be_nil end it "sets resolved_by" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved_by).to eq(current_user) end it "marks as resolved" do subject.resolve!(current_user) + first_note.reload + third_note.reload expect(subject.resolved?).to be true end @@ -451,16 +445,10 @@ describe Discussion, model: true do third_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_at).to be_nil expect(third_note.resolved_at).to be_nil @@ -468,6 +456,8 @@ describe Discussion, model: true do it "unsets resolved_by on the resolved notes" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved_by).to be_nil expect(third_note.resolved_by).to be_nil @@ -475,6 +465,8 @@ describe Discussion, model: true do it "unmarks the resolved notes as resolved" do subject.unresolve! + first_note.reload + third_note.reload expect(first_note.resolved?).to be false expect(third_note.resolved?).to be false @@ -482,12 +474,16 @@ describe Discussion, model: true do it "unsets resolved_at" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_at).to be_nil end it "unsets resolved_by" do subject.unresolve! + first_note.reload + third_note.reload expect(subject.resolved_by).to be_nil end @@ -504,40 +500,22 @@ describe Discussion, model: true do first_note.resolve!(user) end - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! - end - it "unsets resolved_at on the resolved note" do subject.unresolve! - expect(first_note.resolved_at).to be_nil + expect(subject.first_note.resolved_at).to be_nil end it "unsets resolved_by on the resolved note" do subject.unresolve! - expect(first_note.resolved_by).to be_nil + expect(subject.first_note.resolved_by).to be_nil end it "unmarks the resolved note as resolved" do subject.unresolve! - expect(first_note.resolved?).to be false - end - end - - context "when no resolvable notes are resolved" do - it "calls unresolve! on every resolvable note" do - expect(first_note).to receive(:unresolve!) - expect(second_note).not_to receive(:unresolve!) - expect(third_note).to receive(:unresolve!) - - subject.unresolve! + expect(subject.first_note.resolved?).to be false end end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 913d74645a7..be57957b569 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -71,9 +71,6 @@ describe ProjectMember, models: true do describe :import_team do before do - @abilities = Six.new - @abilities << Ability - @project_1 = create :project @project_2 = create :project @@ -92,8 +89,8 @@ describe ProjectMember, models: true do it { expect(@project_2.users).to include(@user_1) } it { expect(@project_2.users).to include(@user_2) } - it { expect(@abilities.allowed?(@user_1, :create_project, @project_2)).to be_truthy } - it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_1, :create_project, @project_2)).to be_truthy } + it { expect(Ability.allowed?(@user_2, :read_project, @project_2)).to be_truthy } end describe 'project 1 should not be changed' do 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/note_spec.rb b/spec/models/note_spec.rb index ef2747046b9..e6b6e7c0634 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -85,8 +85,6 @@ describe Note, models: true do @u1 = create(:user) @u2 = create(:user) @u3 = create(:user) - @abilities = Six.new - @abilities << Ability end describe 'read' do @@ -95,9 +93,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) end - it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :read_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :read_note, @p1)).to be_falsey } end describe 'write' do @@ -106,9 +104,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) end - it { expect(@abilities.allowed?(@u1, :create_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :create_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :create_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :create_note, @p1)).to be_falsey } end describe 'admin' do @@ -118,9 +116,9 @@ describe Note, models: true do @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) end - it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } - it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } - it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey } + it { expect(Ability.allowed?(@u2, :admin_note, @p1)).to be_truthy } + it { expect(Ability.allowed?(@u3, :admin_note, @p1)).to be_falsey } end end @@ -225,7 +223,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_feature_spec.rb b/spec/models/project_feature_spec.rb new file mode 100644 index 00000000000..8d554a01be5 --- /dev/null +++ b/spec/models/project_feature_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe ProjectFeature do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe '#feature_available?' do + let(:features) { %w(issues wiki builds merge_requests snippets) } + + context 'when features are disabled' do + it "returns false" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + end + + context 'when features are enabled only for team members' do + it "returns false when user is not a team member" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(false) + end + end + + it "returns true when user is a team member" do + project.team << [user, :developer] + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true when user is a member of project group" do + group = create(:group) + project = create(:project, namespace: group) + group.add_developer(user) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + + it "returns true if user is an admin" do + user.update_attribute(:admin, true) + + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + + context 'when feature is enabled for everyone' do + it "returns true" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.feature_available?(:issues, user)).to eq(true) + end + end + end + end + + describe '#*_enabled?' do + let(:features) { %w(wiki builds merge_requests) } + + it "returns false when feature is disabled" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(false) + end + end + + it "returns true when feature is enabled only for team members" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + + it "returns true when feature is enabled for everyone" do + features.each do |feature| + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) + expect(project.public_send("#{feature}_enabled?")).to eq(true) + end + end + end +end diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb deleted file mode 100644 index 36379074ea0..00000000000 --- a/spec/models/project_security_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'spec_helper' - -describe Project, models: true do - describe 'authorization' do - before do - @p1 = create(:project) - - @u1 = create(:user) - @u2 = create(:user) - @u3 = create(:user) - @u4 = @p1.owner - - @abilities = Six.new - @abilities << Ability - end - - let(:guest_actions) { Ability.project_guest_rules } - let(:report_actions) { Ability.project_report_rules } - let(:dev_actions) { Ability.project_dev_rules } - let(:master_actions) { Ability.project_master_rules } - let(:owner_actions) { Ability.project_owner_rules } - - describe "Non member rules" do - it "denies for non-project users any actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey - end - end - end - - describe "Guest Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST) - end - - it "allows for project user any guest actions" do - guest_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Report Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - end - - it "allows for project user any report actions" do - report_actions.each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy - end - end - end - - describe "Developer Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER) - end - - it "denies for developer master-specific actions" do - [dev_actions - report_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any dev actions" do - dev_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Master Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for developer master-specific actions" do - [master_actions - dev_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project user any master actions" do - master_actions.each do |action| - expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy - end - end - end - - describe "Owner Rules" do - before do - @p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::DEVELOPER) - @p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER) - end - - it "denies for masters admin-specific actions" do - [owner_actions - master_actions].each do |action| - expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey - end - end - - it "allows for project owner any admin actions" do - owner_actions.each do |action| - expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy - end - end - end - end -end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb index 7fcfdf0eacd..452f4e2782c 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -10,7 +10,7 @@ describe SlackService::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'example.gitlab.com', commit: { status: status, @@ -20,42 +20,38 @@ describe SlackService::BuildMessage do } end - context 'succeeded' do + let(:message) { build_message } + + context 'build succeeded' do let(:status) { 'success' } let(:color) { 'good' } let(:duration) { 10 } - + let(:message) { build_message('passed') } + it 'returns a message with information about succeeded build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end end - context 'failed' do + context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } let(:duration) { 10 } it 'returns a message with information about failed build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end - end - - describe '#seconds_name' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 1 } + end - it 'returns seconds as singular when there is only one' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second' - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end + def build_message(status_text = status) + "<example.gitlab.com|project_name>:" \ + " Commit <example.gitlab.com/commit/" \ + "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb new file mode 100644 index 00000000000..babb3909f56 --- /dev/null +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe SlackService::PipelineMessage do + subject { SlackService::PipelineMessage.new(args) } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'example.gitlab.com' }, + commit: { author_name: 'hacker' } + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + def build_message(status_text = status) + "<example.gitlab.com|project_name>:" \ + " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \ + " of <example.gitlab.com/commits/develop|develop> branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 28af68d13b4..5afdc4b2f7b 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -21,6 +21,9 @@ require 'spec_helper' describe SlackService, models: true do + let(:slack) { SlackService.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -42,15 +45,14 @@ describe SlackService, models: true do end describe "Execute" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + let(:push_sample_data) do Gitlab::DataBuilder::Push.build_sample(project, user) end - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } before do allow(slack).to receive_messages( @@ -212,10 +214,8 @@ describe SlackService, models: true do end describe "Note events" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id) } - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } before do allow(slack).to receive_messages( @@ -285,4 +285,63 @@ describe SlackService, models: true do end end end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(slack).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = slack.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + slack.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack API' + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9a3660012f9..4a41fafb84d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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) } + 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,31 +697,43 @@ 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 describe '#builds_enabled' do let(:project) { create :project } - before { project.builds_enabled = true } - subject { project.builds_enabled } it { expect(project.builds_enabled?).to be_truthy } @@ -1442,4 +1466,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 1fea50ad42c..7624050878e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -382,6 +382,24 @@ describe Repository, models: true do end end + describe '#find_branch' do + it 'loads a branch with a fresh repo' do + expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original + + 2.times do + expect(repository.find_branch('feature')).not_to be_nil + end + end + + it 'loads a branch with a cached repo' do + expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original + + 2.times do + expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil + end + end + end + describe '#rm_branch' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:blank_sha) { '0000000000000000000000000000000000000000' } @@ -423,43 +441,77 @@ describe Repository, models: true do end end - describe '#commit_with_hooks' do + describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature'). + with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature'). and_yield.and_return(true) end it 'runs without errors' do expect do - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do expect(repository).to receive(:update_autocrlf_option) - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end context "when the branch wasn't empty" do it 'updates the head' do expect(repository.find_branch('feature').target.id).to eq(old_rev) - repository.commit_with_hooks(user, 'feature') { sample_commit.id } - expect(repository.find_branch('feature').target.id).to eq(sample_commit.id) + repository.update_branch_with_hooks(user, 'feature') { new_rev } + expect(repository.find_branch('feature').target.id).to eq(new_rev) end end end + context 'when the update adds more than one commit' do + it 'runs without errors' do + old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + + # old_rev is an ancestor of new_rev + expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) + + # old_rev is not a direct ancestor (parent) of new_rev + expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev) + + branch = 'feature-ff-target' + repository.add_branch(user, branch, old_rev) + + expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error + end + end + + context 'when the update would remove commits from the target branch' do + it 'raises an exception' do + branch = 'master' + old_rev = repository.find_branch(branch).target.sha + + # The 'master' branch is NOT an ancestor of new_rev. + expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) + + # Updating 'master' to new_rev would lose the commits on 'master' that + # are not contained in new_rev. This should not be allowed. + expect do + repository.update_branch_with_hooks(user, branch) { new_rev } + end.to raise_error(Repository::CommitError) + end + end + context 'when pre hooks failed' do it 'gets an error' do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.commit_with_hooks(user, 'feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'feature') { new_rev } end.to raise_error(GitHooksService::PreReceiveError) end end @@ -467,6 +519,7 @@ describe Repository, models: true do context 'when target branch is different from source branch' do before do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + allow(repository).to receive(:update_ref!) end it 'expires branch cache' do @@ -477,7 +530,7 @@ describe Repository, models: true do expect(repository).to receive(:expire_has_visible_content_cache) expect(repository).to receive(:expire_branch_count_cache) - repository.commit_with_hooks(user, 'new-feature') { sample_commit.id } + repository.update_branch_with_hooks(user, 'new-feature') { new_rev } end end @@ -1250,4 +1303,18 @@ describe Repository, models: true do File.delete(path) end end + + describe '#update_ref!' do + it 'can create a ref' do + repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + + expect(repository.find_branch('foobar')).not_to be_nil + end + + it 'raises CommitError when the ref update fails' do + expect do + repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + end.to raise_error(Repository::CommitError) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8eb0c5033c9..a1770d96f83 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1006,8 +1006,7 @@ describe User, models: true do end it 'does not include projects for which issues are disabled' do - project = create(:project) - project.update_attributes(issues_enabled: false) + project = create(:project, issues_access_level: ProjectFeature::DISABLED) expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.can?(:admin_issue, project)).to eq(false) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb new file mode 100644 index 00000000000..eda1cafd65e --- /dev/null +++ b/spec/policies/project_policy_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ProjectPolicy, models: true do + let(:project) { create(:empty_project, :public) } + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:dev) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + + let(:users_ordered_by_permissions) do + [nil, guest, reporter, dev, master, owner, admin] + end + + let(:users_permissions) do + users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size } + end + + before do + project.team << [guest, :guest] + project.team << [master, :master] + project.team << [dev, :developer] + project.team << [reporter, :reporter] + + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(owner) + end + + it 'returns increasing permissions for each level' do + expect(users_permissions).to eq(users_permissions.sort.uniq) + 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/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb new file mode 100644 index 00000000000..7c9078b2864 --- /dev/null +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe API::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /broadcast_messages' do + it 'returns a 401 for anonymous users' do + get api('/broadcast_messages') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/broadcast_messages', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of BroadcastMessages for admins' do + create(:broadcast_message) + + get api('/broadcast_messages', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'GET /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + get api("/broadcast_messages/#{message.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api("/broadcast_messages/#{message.id}", user) + + expect(response).to have_http_status(403) + end + + it 'returns the specified message for admins' do + get api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq message.id + expect(json_response.keys) + .to match_array(%w(id message starts_at ends_at color font active)) + end + end + + describe 'POST /broadcast_messages' do + it 'returns a 401 for anonymous users' do + post api('/broadcast_messages'), attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + post api('/broadcast_messages', user), attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'requires the `message` parameter' do + attrs = attributes_for(:broadcast_message) + attrs.delete(:message) + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'message is missing' + end + + it 'defines sane default start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + post api('/broadcast_messages', admin), message: 'Test message' + + expect(response).to have_http_status(201) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z' + end + end + + it 'accepts a custom background and foreground color' do + attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece') + + post api('/broadcast_messages', admin), attrs + + expect(response).to have_http_status(201) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + end + end + + describe 'PUT /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + put api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + put api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + context 'as an admin' do + it 'accepts new background and foreground colors' do + attrs = { color: '#000000', font: '#cecece' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['color']).to eq attrs[:color] + expect(json_response['font']).to eq attrs[:font] + end + + it 'accepts new start and end times' do + time = Time.zone.parse('2016-07-02 10:11:12') + travel_to(time) do + attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' + expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z' + end + end + + it 'accepts a new message' do + attrs = { message: 'new message' } + + put api("/broadcast_messages/#{message.id}", admin), attrs + + expect(response).to have_http_status(200) + expect { message.reload }.to change { message.message }.to('new message') + end + end + end + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + delete api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + it 'deletes the broadcast message for admins' do + expect { delete api("/broadcast_messages/#{message.id}", admin) } + .to change { BroadcastMessage.count }.by(-1) + end + end +end 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/fork_spec.rb b/spec/requests/api/fork_spec.rb index f802fcd2d2e..06e3a2183c0 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -6,6 +6,12 @@ describe API::API, api: true do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } + let(:group) { create(:group) } + let(:group2) do + group = create(:group, name: 'group2_name') + group.add_owner(user2) + group + end let(:project) do create(:project, creator_id: user.id, namespace: user.namespace) @@ -22,6 +28,7 @@ describe API::API, api: true do context 'when authenticated' do it 'forks if user has sufficient access to project' do post api("/projects/fork/#{project.id}", user2) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -32,6 +39,7 @@ describe API::API, api: true do it 'forks if user is admin' do post api("/projects/fork/#{project.id}", admin) + expect(response).to have_http_status(201) expect(json_response['name']).to eq(project.name) expect(json_response['path']).to eq(project.path) @@ -42,12 +50,14 @@ describe API::API, api: true do it 'fails on missing project access for the project to fork' do post api("/projects/fork/#{project.id}", user3) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end it 'fails if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) + expect(response).to have_http_status(409) expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken']) @@ -55,14 +65,70 @@ describe API::API, api: true do it 'fails if project to fork from does not exist' do post api('/projects/fork/424242', user) + expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Project Not Found') end + + it 'forks with explicit own user namespace id' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks with explicit own user name as namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'forks to another user when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: user2.username + + expect(response).to have_http_status(201) + expect(json_response['owner']['id']).to eq(user2.id) + end + + it 'fails if trying to fork to another user when not admin' do + post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id + + expect(response).to have_http_status(409) + end + + it 'fails if trying to fork to non-existent namespace' do + post api("/projects/fork/#{project.id}", user2), namespace: 42424242 + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Target Namespace Not Found') + end + + it 'forks to owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group2.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group2.name) + end + + it 'fails to fork to not owned group' do + post api("/projects/fork/#{project.id}", user2), namespace: group.name + + expect(response).to have_http_status(409) + end + + it 'forks to not owned group when admin' do + post api("/projects/fork/#{project.id}", admin), namespace: group.name + + expect(response).to have_http_status(201) + expect(json_response['namespace']['name']).to eq(group.name) + end end context 'when unauthenticated' do it 'returns authentication error' do post api("/projects/fork/#{project.id}") + expect(response).to have_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 5d06abcfeb3..46d1b868782 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -44,8 +44,8 @@ describe API::API, api: true do secret_token: secret_token, key_id: 12345 - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Not found') + expect(json_response['success']).to be_falsey + expect(json_response['message']).to eq('Could not find the given key') end it 'returns an error message when the key is a deploy key' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index b8038fc85a1..86d994be079 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) } @@ -16,21 +17,24 @@ describe API::API, api: true do assignee: user, project: project, state: :closed, - milestone: milestone + milestone: milestone, + updated_at: 3.hours.ago end let!(:confidential_issue) do create :issue, :confidential, project: project, author: author, - assignee: assignee + assignee: assignee, + updated_at: 2.hours.ago end let!(:issue) do create :issue, author: user, assignee: user, project: project, - milestone: milestone + milestone: milestone, + updated_at: 1.hour.ago end let!(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) @@ -134,6 +138,42 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(0) end + + it 'sorts by created_at descending by default' do + get api('/issues', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api('/issues?sort=asc', user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api('/issues?order_by=updated_at', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api('/issues?order_by=updated_at&sort=asc', user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end end @@ -146,21 +186,24 @@ describe API::API, api: true do assignee: user, project: group_project, state: :closed, - milestone: group_milestone + milestone: group_milestone, + updated_at: 3.hours.ago end let!(:group_confidential_issue) do create :issue, :confidential, project: group_project, author: author, - assignee: assignee + assignee: assignee, + updated_at: 2.hours.ago end let!(:group_issue) do create :issue, author: user, assignee: user, project: group_project, - milestone: group_milestone + milestone: group_milestone, + updated_at: 1.hour.ago end let!(:group_label) do create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) @@ -277,6 +320,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api(base_url, user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues" do @@ -385,6 +464,42 @@ describe API::API, api: true do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) end + + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get api("#{base_url}/issues?sort=asc", user) + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get api("#{base_url}/issues?order_by=updated_at", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end end describe "GET /projects/:id/issues/:issue_id" do @@ -404,6 +519,7 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy end it "returns a project issue by id" do @@ -469,13 +585,63 @@ describe API::API, api: true do end describe "POST /projects/:id/issues" do - it "creates a new project issue" do + it 'creates a new project issue' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' + expect(response).to have_http_status(201) expect(json_response['title']).to eq('new issue') expect(json_response['description']).to be_nil expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['confidential']).to be_falsy + end + + it 'creates a new confidential project issue' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'y' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: false + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + 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 @@ -619,6 +785,30 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end + + it 'sets an issue to confidential' do + put api("/projects/#{project.id}/issues/#{issue.id}", user), + confidential: true + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: false + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end end end @@ -633,6 +823,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/lint_spec.rb b/spec/requests/api/lint_spec.rb new file mode 100644 index 00000000000..391fc13a380 --- /dev/null +++ b/spec/requests/api/lint_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe API::Lint, api: true do + include ApiHelpers + + describe 'POST /ci/lint' do + context 'with valid .gitlab-ci.yaml content' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'passes validation' do + post api('/ci/lint'), { content: yaml_content } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Hash + expect(json_response['status']).to eq('valid') + expect(json_response['errors']).to eq([]) + end + end + + context 'with an invalid .gitlab_ci.yml' do + it 'responds with errors about invalid syntax' do + post api('/ci/lint'), { content: 'invalid content' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['Invalid configuration format']) + end + + it "responds with errors about invalid configuration" do + post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' } + + expect(response).to have_http_status(200) + expect(json_response['status']).to eq('invalid') + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'without the content parameter' do + it 'responds with validation error about missing content' do + post api('/ci/lint') + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('content is missing') + end + end + 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 baff872e28e..a7930c59df9 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,7 +9,7 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } @@ -34,6 +34,13 @@ describe API::API, api: true do 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') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) 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/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb new file mode 100644 index 00000000000..e6d8a5ee954 --- /dev/null +++ b/spec/requests/api/notification_settings_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) } + + describe "GET /notification_settings" do + it "returns global notification settings for the current user" do + get api("/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['notification_email']).to eq(user.notification_email) + expect(json_response['level']).to eq(user.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + let(:email) { create(:email, user: user) } + + it "updates global notification settings for the current user" do + put api("/notification_settings", user), { level: 'watch', notification_email: email.email } + + expect(response).to have_http_status(200) + expect(json_response['notification_email']).to eq(email.email) + expect(user.reload.notification_email).to eq(email.email) + expect(json_response['level']).to eq(user.reload.global_notification_setting.level) + end + end + + describe "PUT /notification_settings" do + it "fails on non-user email address" do + put api("/notification_settings", user), { notification_email: 'invalid@example.com' } + + expect(response).to have_http_status(400) + end + end + + describe "GET /groups/:id/notification_settings" do + it "returns group level notification settings for the current user" do + get api("/groups/#{group.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(group).level) + end + end + + describe "PUT /groups/:id/notification_settings" do + it "updates group level notification settings for the current user" do + put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level) + end + end + + describe "GET /projects/:id/notification_settings" do + it "returns project level notification settings for the current user" do + get api("/projects/#{project.id}/notification_settings", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_a Hash + expect(json_response['level']).to eq(user.notification_settings_for(project).level) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "updates project level notification settings for the current user" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true } + + expect(response).to have_http_status(200) + expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level) + expect(json_response['events']['new_note']).to eq(true) + expect(json_response['events']['new_issue']).to eq(false) + end + end + + describe "PUT /projects/:id/notification_settings" do + it "fails on invalid level" do + put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' } + + expect(response).to have_http_status(400) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 63f2467be63..28aa56e8644 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -73,7 +73,7 @@ describe API::API, api: true do end it 'does not include open_issues_count' do - project.update_attributes( { issues_enabled: false } ) + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get api('/projects', user) expect(response.status).to eq 200 @@ -231,8 +231,15 @@ describe API::API, api: true do post api('/projects', user), project project.each_pair do |k, v| + next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k) expect(json_response[k.to_s]).to eq(v) end + + # Check feature permissions attributes + project = Project.find_by_path(project[:path]) + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) end it 'sets a project as public' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 0bbba64a6d5..ef73778efa9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -605,6 +605,7 @@ describe API::API, api: true do expect(json_response['can_create_project']).to eq(user.can_create_project?) expect(json_response['can_create_group']).to eq(user.can_create_group?) expect(json_response['projects_limit']).to eq(user.projects_limit) + expect(json_response['private_token']).to be_blank end it "returns 401 error if user is unauthenticated" do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index ca7932dc5da..9e390bea50b 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -230,7 +230,8 @@ describe Ci::API::API do let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } - let(:headers) { { "GitLab-Workhorse" => "1.0" } } + let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } before { build.run! } @@ -240,14 +241,22 @@ describe Ci::API::API do it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(json_response["TempPath"]).not_to be_nil end it "using token as header" do post authorize_url, {}, headers_with_token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(json_response["TempPath"]).not_to be_nil end + + it "reject requests that did not go through gitlab-workhorse" do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + post authorize_url, { token: build.token }, headers + expect(response).to have_http_status(500) + end end context "should fail to post too large artifact" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index afaf4b7cefb..b7001fede40 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe 'Git HTTP requests', lib: true do + include WorkhorseHelpers + let(:user) { create(:user) } let(:project) { create(:project, path: 'project.git-project') } @@ -48,6 +50,7 @@ describe 'Git HTTP requests', lib: true do expect(response).to have_http_status(200) expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end end @@ -63,6 +66,7 @@ describe 'Git HTTP requests', lib: true do it "downloads get status 200" do download(path, {}) do |response| expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end @@ -101,6 +105,14 @@ describe 'Git HTTP requests', lib: true do end end end + + context 'when the request is not from gitlab-workhorse' do + it 'raises an exception' do + expect do + get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack") + end.to raise_error(JWT::DecodeError) + end + end end context "when the project is private" do @@ -170,11 +182,13 @@ describe 'Git HTTP requests', lib: true do clone_get(path, env) expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 200" do upload(path, env) do |response| expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end end end @@ -189,6 +203,7 @@ describe 'Git HTTP requests', lib: true do clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 401 (no project existence information leak)" do @@ -289,13 +304,15 @@ describe 'Git HTTP requests', lib: true do let(:project) { FactoryGirl.create :empty_project } before do - project.update_attributes(runners_token: token, builds_enabled: true) + project.update_attributes(runners_token: token) + project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) end it "downloads get status 200" do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) end it "uploads get status 401 (no project existence information leak)" do @@ -425,7 +442,7 @@ describe 'Git HTTP requests', lib: true do end def auth_env(user, password, spnego_request_token) - env = {} + env = workhorse_internal_api_request_header if user && password env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password) elsif spnego_request_token diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index c6172b9cc7d..fc42b534dca 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -22,19 +22,20 @@ describe JwtController do context 'when using authorized request' do context 'using CI token' do - let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:project) { create(:empty_project, runners_token: 'token') } let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } - subject! { get '/jwt/auth', parameters, headers } - context 'project with enabled CI' do - let(:builds_enabled) { true } - + subject! { get '/jwt/auth', parameters, headers } it { expect(service_class).to have_received(:new).with(project, nil, parameters) } end context 'project with disabled CI' do - let(:builds_enabled) { false } + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + subject! { get '/jwt/auth', parameters, headers } it { expect(response).to have_http_status(403) } end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 4c9b4a8ba42..6e551bb65fa 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Git LFS API and storage' do + include WorkhorseHelpers + let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -44,6 +46,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) } @@ -608,6 +717,12 @@ describe 'Git LFS API and storage' do project.team << [user, :developer] end + context 'and the request bypassed workhorse' do + it 'raises an exception' do + expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError + end + end + context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize @@ -617,6 +732,10 @@ describe 'Git LFS API and storage' do expect(response).to have_http_status(200) end + it 'uses the gitlab-workhorse content type' do + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + it 'responds with status 200, location of lfs store and object details' do expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") expect(json_response['LfsOid']).to eq(sample_oid) @@ -756,8 +875,11 @@ describe 'Git LFS API and storage' do end end - def put_authorize - put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers + def put_authorize(verified: true) + authorize_headers = headers + authorize_headers.merge!(workhorse_internal_api_request_header) if verified + + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers end def put_finalize(lfs_tmp = lfs_tmp_file) 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 d65648dd0b2..4bc3cddd9c2 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -107,9 +107,9 @@ 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' 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/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 026d0ca6534..1e21a32a062 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -151,6 +151,25 @@ module Ci it { expect(build.runner).to eq(specific_runner) } end end + + context 'disallow when builds are disabled' do + before do + project.update(shared_runners_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + context 'and uses shared runner' do + let(:build) { service.execute(shared_runner) } + + it { expect(build).to be_nil } + end + + context 'and uses specific runner' do + let(:build) { service.execute(specific_runner) } + + it { expect(build).to be_nil } + end + end end end end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index ac08aa53b0b..6f7ce8ca992 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Issues::BulkUpdateService, services: true do +describe Issuable::BulkUpdateService, services: true do let(:user) { create(:user) } let(:project) { create(:empty_project, namespace: user.namespace) } def bulk_update(issues, extra_params = {}) bulk_update_params = extra_params - .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) + .reverse_merge(issuable_ids: Array(issues).map(&:id).join(',')) - Issues::BulkUpdateService.new(project, user, bulk_update_params).execute + Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue') end describe 'close issues' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index aff022a573e..5dfb33f4b28 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -18,12 +18,12 @@ describe Issues::CloseService, services: true do context "valid params" do before do perform_enqueued_jobs do - @issue = described_class.new(project, user, {}).execute(issue) + described_class.new(project, user).execute(issue) end end - it { expect(@issue).to be_valid } - it { expect(@issue).to be_closed } + it { expect(issue).to be_valid } + it { expect(issue).to be_closed } it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last @@ -32,7 +32,7 @@ describe Issues::CloseService, services: true do end it 'creates system note about issue reassign' do - note = @issue.notes.last + note = issue.notes.last expect(note.note).to include "Status changed to closed" end @@ -44,23 +44,43 @@ describe Issues::CloseService, services: true do context 'current user is not authorized to close issue' do before do perform_enqueued_jobs do - @issue = described_class.new(project, guest).execute(issue) + described_class.new(project, guest).execute(issue) end end it 'does not close the issue' do - expect(@issue).to be_open + expect(issue).to be_open end end - context "external issue tracker" do + context 'when issue is not confidential' do + it 'executes issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'when issue is confidential' do + it 'executes confidential issue hooks' do + issue = create(:issue, :confidential, project: project) + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'external issue tracker' do before do allow(project).to receive(:default_issues_tracker?).and_return(false) - @issue = described_class.new(project, user, {}).execute(issue) + described_class.new(project, user).execute(issue) end - it { expect(@issue).to be_valid } - it { expect(@issue).to be_opened } + it { expect(issue).to be_valid } + it { expect(issue).to be_opened } it { expect(todo.reload).to be_pending } end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index fcc3c0a00bd..58569ba96c3 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -72,6 +72,24 @@ describe Issues::CreateService, services: true do expect(issue.milestone).not_to eq milestone end end + + it 'executes issue hooks when issue is not confidential' do + opts = { title: 'Title', description: 'Description', confidential: false } + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user, opts).execute + end + + it 'executes confidential issue hooks when issue is confidential' do + opts = { title: 'Title', description: 'Description', confidential: true } + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user, opts).execute + end end it_behaves_like 'new issuable record that supports slash commands' diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 34a89fcd4e1..93a8270fd16 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -1,24 +1,50 @@ require 'spec_helper' describe Issues::ReopenService, services: true do - let(:guest) { create(:user) } - let(:issue) { create(:issue, :closed) } - let(:project) { issue.project } - - before do - project.team << [guest, :guest] - end + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, :closed, project: project) } describe '#execute' do - context 'current user is not authorized to reopen issue' do + context 'when user is not authorized to reopen issue' do before do + guest = create(:user) + project.team << [guest, :guest] + perform_enqueued_jobs do - @issue = described_class.new(project, guest).execute(issue) + described_class.new(project, guest).execute(issue) end end it 'does not reopen the issue' do - expect(@issue).to be_closed + expect(issue).to be_closed + end + end + + context 'when user is authrized to reopen issue' do + let(:user) { create(:user) } + + before do + project.team << [user, :master] + end + + context 'when issue is not confidential' do + it 'executes issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) + + described_class.new(project, user).execute(issue) + end + end + + context 'when issue is confidential' do + it 'executes confidential issue hooks' do + issue = create(:issue, :confidential, :closed, project: project) + + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + described_class.new(project, user).execute(issue) + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 0313f424463..4f5375a3583 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -23,11 +23,15 @@ describe Issues::UpdateService, services: true do describe 'execute' do def find_note(starting_with) - @issue.notes.find do |note| + issue.notes.find do |note| note && note.note.start_with?(starting_with) end end + def update_issue(opts) + described_class.new(project, user, opts).execute(issue) + end + context "valid params" do before do opts = { @@ -35,23 +39,20 @@ describe Issues::UpdateService, services: true do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id], - confidential: true + label_ids: [label.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + update_issue(opts) end - - @issue.reload end - it { expect(@issue).to be_valid } - it { expect(@issue.title).to eq('New title') } - it { expect(@issue.assignee).to eq(user2) } - it { expect(@issue).to be_closed } - it { expect(@issue.labels.count).to eq(1) } - it { expect(@issue.labels.first.title).to eq(label.name) } + it { expect(issue).to be_valid } + it { expect(issue.title).to eq('New title') } + it { expect(issue.assignee).to eq(user2) } + it { expect(issue).to be_closed } + it { expect(issue.labels.count).to eq(1) } + it { expect(issue.labels.first.title).to eq(label.name) } it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do deliveries = ActionMailer::Base.deliveries @@ -81,18 +82,35 @@ describe Issues::UpdateService, services: true do expect(note).not_to be_nil expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' end + end + + context 'when issue turns confidential' do + let(:opts) do + { + title: 'New title', + description: 'Also please fix', + assignee_id: user2.id, + state_event: 'close', + label_ids: [label.id], + confidential: true + } + end it 'creates system note about confidentiality change' do + update_issue(confidential: true) + note = find_note('Made the issue confidential') expect(note).not_to be_nil expect(note.note).to eq 'Made the issue confidential' end - end - def update_issue(opts) - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) - @issue.reload + it 'executes confidential issue hooks' do + expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) + expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) + + update_issue(confidential: true) + end end context 'todos' do @@ -100,7 +118,7 @@ describe Issues::UpdateService, services: true do context 'when the title change' do before do - update_issue({ title: 'New title' }) + update_issue(title: 'New title') end it 'marks pending todos as done' do @@ -110,7 +128,7 @@ describe Issues::UpdateService, services: true do context 'when the description change' do before do - update_issue({ description: 'Also please fix' }) + update_issue(description: 'Also please fix') end it 'marks todos as done' do @@ -120,7 +138,7 @@ describe Issues::UpdateService, services: true do context 'when is reassigned' do before do - update_issue({ assignee: user2 }) + update_issue(assignee: user2) end it 'marks previous assignee todos as done' do @@ -144,7 +162,7 @@ describe Issues::UpdateService, services: true do context 'when the milestone change' do before do - update_issue({ milestone: create(:milestone) }) + update_issue(milestone: create(:milestone)) end it 'marks todos as done' do @@ -154,7 +172,7 @@ describe Issues::UpdateService, services: true do context 'when the labels change' do before do - update_issue({ label_ids: [label.id] }) + update_issue(label_ids: [label.id]) end it 'marks todos as done' do @@ -165,6 +183,7 @@ describe Issues::UpdateService, services: true do context 'when the issue is relabeled' do let!(:non_subscriber) { create(:user) } + let!(:subscriber) do create(:user).tap do |u| label.toggle_subscription(u) @@ -176,7 +195,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_email(subscriber) @@ -190,7 +209,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_not_email(subscriber) @@ -201,7 +220,7 @@ describe Issues::UpdateService, services: true do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue = described_class.new(project, user, opts).execute(issue) end should_not_email(subscriber) @@ -210,13 +229,15 @@ describe Issues::UpdateService, services: true do end end - context 'when Issue has tasks' do - before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } + context 'when issue has tasks' do + before do + update_issue(description: "- [ ] Task 1\n- [ ] Task 2") + end - it { expect(@issue.tasks?).to eq(true) } + it { expect(issue.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) } + before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } it 'creates system note about task status change' do note1 = find_note('Marked the task **Task 1** as completed') @@ -229,8 +250,8 @@ describe Issues::UpdateService, services: true do context 'when tasks are marked as incomplete' do before do - update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) + update_issue(description: "- [x] Task 1\n- [X] Task 2") + update_issue(description: "- [ ] Task 1\n- [ ] Task 2") end it 'creates system note about task status change' do @@ -244,8 +265,8 @@ describe Issues::UpdateService, services: true do context 'when tasks position has been modified' do before do - update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" }) + update_issue(description: "- [x] Task 1\n- [X] Task 2") + update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2") end it 'does not create a system note' do @@ -257,8 +278,8 @@ describe Issues::UpdateService, services: true do context 'when a Task list with a completed item is totally replaced' do before do - update_issue({ description: "- [ ] Task 1\n- [X] Task 2" }) - update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" }) + update_issue(description: "- [ ] Task 1\n- [X] Task 2") + update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three") end it 'does not create a system note referencing the position the old item' do @@ -269,7 +290,7 @@ describe Issues::UpdateService, services: true do it 'does not generate a new note at all' do expect do - update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" }) + update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three") end.not_to change { Note.count } end end @@ -277,7 +298,7 @@ describe Issues::UpdateService, services: true do context 'updating labels' do let(:label3) { create(:label, project: project) } - let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload } + let(:result) { described_class.new(project, user, params).execute(issue).reload } context 'when add_label_ids and label_ids are passed' do let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 232508cda23..0d586e2216b 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -99,14 +99,14 @@ describe MergeRequests::BuildService, services: true do let(:source_branch) { "#{issue.iid}-fix-issue" } it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}") + expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}") end context 'merge request already has a description set' do let(:description) { 'Merge request description' } it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}") + expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}") end end diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 8a4b76367e3..3a71776e81f 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -50,7 +50,7 @@ describe MergeRequests::GetUrlsService do let(:changes) { new_branch_changes } before do - project.merge_requests_enabled = false + project.project_feature.update_attribute(:merge_requests_access_level, ProjectFeature::DISABLED) end it_behaves_like 'no_merge_request_url' 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/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index bbced59ff02..3ea1273abc3 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!(wiki_enabled: false) + @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end @@ -85,7 +85,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled false does not enable CI by default' do before do - @opts.merge!(builds_enabled: false) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end it { is_expected.to be_falsey } @@ -93,7 +93,7 @@ describe Projects::CreateService, services: true do context 'global builds_enabled true does enable CI by default' do before do - @opts.merge!(builds_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end it { is_expected.to be_truthy } 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..b41f6f14fbd 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -145,6 +145,14 @@ describe TodoService, services: true do end end + describe '#destroy_issue' do + it 'refresh the todos count cache for the user' do + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.destroy_issue(issue, john_doe) + end + end + describe '#reassigned_issue' do it 'creates a pending todo for new assignee' do unassigned_issue.update_attribute(:assignee, john_doe) @@ -394,6 +402,14 @@ describe TodoService, services: true do end end + describe '#destroy_merge_request' do + it 'refresh the todos count cache for the user' do + expect(john_doe).to receive(:update_todos_count_cache).and_call_original + + service.destroy_merge_request(mr_assigned, john_doe) + end + end + describe '#reassigned_merge_request' do it 'creates a pending todo for new assignee' do mr_unassigned.update_attribute(:assignee, john_doe) @@ -496,6 +512,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 +535,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/spec_helper.rb b/spec/spec_helper.rb index c144cd85487..02b2b3ca101 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,9 +26,9 @@ 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 diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index e0dbc9aa84c..ac38e31b77e 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -15,7 +15,7 @@ RSpec.configure do |config| DatabaseCleaner.start end - config.after(:each) do + config.append_after(:each) do DatabaseCleaner.clean end end diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb index d2a49ea5c5e..5e3b8f2b23e 100644 --- a/spec/support/issuable_slash_commands_shared_examples.rb +++ b/spec/support/issuable_slash_commands_shared_examples.rb @@ -2,6 +2,9 @@ # It takes a `issuable_type`, and expect an `issuable`. shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type| + include SlashCommandsHelpers + include WaitForAjax + let(:master) { create(:user) } let(:assignee) { create(:user, username: 'bob') } let(:guest) { create(:user) } @@ -18,6 +21,11 @@ shared_examples 'issuable record that supports slash commands in its description login_with(master) end + after do + # Ensure all outstanding Ajax requests are complete to avoid database deadlocks + wait_for_ajax + end + describe "new #{issuable_type}" do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do @@ -44,10 +52,7 @@ shared_examples 'issuable record that supports slash commands in its description context 'with a note containing commands' do it 'creates a note without the commands and interpret the commands accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"" - click_button 'Comment' - end + write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).to have_content 'Awesome!' expect(page).not_to have_content '/assign @bob' @@ -66,10 +71,7 @@ shared_examples 'issuable record that supports slash commands in its description context 'with a note containing only commands' do it 'does not create a note but interpret the commands accordingly' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\"" - click_button 'Comment' - end + write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).not_to have_content '/assign @bob' expect(page).not_to have_content '/label ~bug' @@ -92,10 +94,7 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user can close #{issuable_type}" do it "closes the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/close" - click_button 'Comment' - end + write_note("/close") expect(page).not_to have_content '/close' expect(page).to have_content 'Your commands have been executed!' @@ -112,10 +111,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not close the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/close" - click_button 'Comment' - end + write_note("/close") expect(page).not_to have_content '/close' expect(page).not_to have_content 'Your commands have been executed!' @@ -133,10 +129,7 @@ shared_examples 'issuable record that supports slash commands in its description context "when current user can reopen #{issuable_type}" do it "reopens the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/reopen" - click_button 'Comment' - end + write_note("/reopen") expect(page).not_to have_content '/reopen' expect(page).to have_content 'Your commands have been executed!' @@ -153,10 +146,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not reopen the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/reopen" - click_button 'Comment' - end + write_note("/reopen") expect(page).not_to have_content '/reopen' expect(page).not_to have_content 'Your commands have been executed!' @@ -169,10 +159,7 @@ shared_examples 'issuable record that supports slash commands in its description context "with a note changing the #{issuable_type}'s title" do context "when current user can change title of #{issuable_type}" do it "reopens the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/title Awesome new title" - click_button 'Comment' - end + write_note("/title Awesome new title") expect(page).not_to have_content '/title' expect(page).to have_content 'Your commands have been executed!' @@ -189,10 +176,7 @@ shared_examples 'issuable record that supports slash commands in its description end it "does not reopen the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/title Awesome new title" - click_button 'Comment' - end + write_note("/title Awesome new title") expect(page).not_to have_content '/title' expect(page).not_to have_content 'Your commands have been executed!' @@ -204,10 +188,7 @@ shared_examples 'issuable record that supports slash commands in its description context "with a note marking the #{issuable_type} as todo" do it "creates a new todo for the #{issuable_type}" do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/todo" - click_button 'Comment' - end + write_note("/todo") expect(page).not_to have_content '/todo' expect(page).to have_content 'Your commands have been executed!' @@ -238,10 +219,7 @@ shared_examples 'issuable record that supports slash commands in its description expect(todo.author).to eq master expect(todo.user).to eq master - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/done" - click_button 'Comment' - end + write_note("/done") expect(page).not_to have_content '/done' expect(page).to have_content 'Your commands have been executed!' @@ -254,10 +232,7 @@ shared_examples 'issuable record that supports slash commands in its description it "creates a new todo for the #{issuable_type}" do expect(issuable.subscribed?(master)).to be_falsy - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/subscribe" - click_button 'Comment' - end + write_note("/subscribe") expect(page).not_to have_content '/subscribe' expect(page).to have_content 'Your commands have been executed!' @@ -274,10 +249,7 @@ shared_examples 'issuable record that supports slash commands in its description it "creates a new todo for the #{issuable_type}" do expect(issuable.subscribed?(master)).to be_truthy - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "/unsubscribe" - click_button 'Comment' - end + write_note("/unsubscribe") expect(page).not_to have_content '/unsubscribe' expect(page).to have_content 'Your commands have been executed!' diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb new file mode 100644 index 00000000000..079f244475c --- /dev/null +++ b/spec/support/ldap_helpers.rb @@ -0,0 +1,47 @@ +module LdapHelpers + def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap)) + ::Gitlab::LDAP::Adapter.new(provider, ldap) + end + + def user_dn(uid) + "uid=#{uid},ou=users,dc=example,dc=com" + end + + # Accepts a hash of Gitlab::LDAP::Config keys and values. + # + # Example: + # stub_ldap_config( + # group_base: 'ou=groups,dc=example,dc=com', + # admin_group: 'my-admin-group' + # ) + def stub_ldap_config(messages) + messages.each do |config, value| + allow_any_instance_of(::Gitlab::LDAP::Config) + .to receive(config.to_sym).and_return(value) + end + end + + # Stub an LDAP person search and provide the return entry. Specify `nil` for + # `entry` to simulate when an LDAP person is not found + # + # Example: + # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) + # ldap_user_entry = ldap_user_entry('john_doe') + # + # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter) + def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain') + return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present? + + allow(::Gitlab::LDAP::Person) + .to receive(:find_by_uid).with(uid, any_args).and_return(return_value) + end + + # Create a simple LDAP user entry. + def ldap_user_entry(uid) + entry = Net::LDAP::Entry.new + entry['dn'] = user_dn(uid) + entry['uid'] = uid + + entry + end +end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index e5f76afbfc0..c0b3e83244d 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -75,6 +75,7 @@ module LoginHelpers def logout find(".header-user-dropdown-toggle").click click_link "Sign out" + expect(page).to have_content('Signed out successfully') end # Logout without JavaScript driver diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb new file mode 100644 index 00000000000..df483afa0e3 --- /dev/null +++ b/spec/support/slash_commands_helpers.rb @@ -0,0 +1,10 @@ +module SlashCommandsHelpers + def write_note(text) + Sidekiq::Testing.fake! do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: text + click_button 'Comment' + end + end + end +end 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/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb index 107b6e30924..47673cd4c3a 100644 --- a/spec/support/workhorse_helpers.rb +++ b/spec/support/workhorse_helpers.rb @@ -13,4 +13,9 @@ module WorkhorseHelpers ] end end + + def workhorse_internal_api_request_header + jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') + { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token } + end end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 464051063d8..446ba3bfa14 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -59,14 +59,10 @@ describe 'projects/builds/show' do end it 'shows trigger variables in separate lines' do - expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1')) - expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2')) + expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1') + expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2') + expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1') + expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end - - private - - def variable_regexp(key, value) - /\A#{Regexp.escape("#{key}=#{value}")}\Z/ - 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/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb new file mode 100644 index 00000000000..35e1518a35e --- /dev/null +++ b/spec/workers/prune_old_events_worker_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe PruneOldEventsWorker do + describe '#perform' do + let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) } + let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) } + let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) } + + it 'prunes events older than 12 months' do + expect { subject.perform }.to change { Event.count }.by(-1) + expect(Event.find_by(id: expired_event.id)).to be_nil + end + + it 'leaves fresh events' do + subject.perform + expect(not_expired_event.reload).to be_present + end + + it 'leaves events from exactly 12 months ago' do + subject.perform + expect(exactly_12_months_event).to be_present + end + end +end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 05e07789dac..59cfb2c8e3a 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } it 'passes when the project has no push events' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) project.events.destroy_all break_repo(project) @@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'fails if the wiki repository is broken' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) project.create_wiki # Test sanity: everything should be fine before the wiki repo is broken @@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'skips wikis when disabled' do - project = create(:project_empty_repo, wiki_enabled: false) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) # Make sure the test would fail if the wiki repo was checked break_wiki(project) @@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'creates missing wikis' do - project = create(:project_empty_repo, wiki_enabled: true) + project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) FileUtils.rm_rf(wiki_path(project)) subject.perform(project.id) diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js index bc451506b6a..9fbfef03f6d 100644 --- a/vendor/assets/javascripts/task_list.js +++ b/vendor/assets/javascripts/task_list.js @@ -1,15 +1,118 @@ - +// The MIT License (MIT) +// +// Copyright (c) 2014 GitHub, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// TaskList Behavior +// /*= provides tasklist:enabled */ - - /*= provides tasklist:disabled */ - - /*= provides tasklist:change */ - - /*= provides tasklist:changed */ - +// +// +// Enables Task List update behavior. +// +// ### Example Markup +// +// <div class="js-task-list-container"> +// <ul class="task-list"> +// <li class="task-list-item"> +// <input type="checkbox" class="js-task-list-item-checkbox" disabled /> +// text +// </li> +// </ul> +// <form> +// <textarea class="js-task-list-field">- [ ] text</textarea> +// </form> +// </div> +// +// ### Specification +// +// TaskLists MUST be contained in a `(div).js-task-list-container`. +// +// TaskList Items SHOULD be an a list (`UL`/`OL`) element. +// +// Task list items MUST match `(input).task-list-item-checkbox` and MUST be +// `disabled` by default. +// +// TaskLists MUST have a `(textarea).js-task-list-field` form element whose +// `value` attribute is the source (Markdown) to be udpated. The source MUST +// follow the syntax guidelines. +// +// TaskList updates trigger `tasklist:change` events. If the change is +// successful, `tasklist:changed` is fired. The change can be canceled. +// +// jQuery is required. +// +// ### Methods +// +// `.taskList('enable')` or `.taskList()` +// +// Enables TaskList updates for the container. +// +// `.taskList('disable')` +// +// Disables TaskList updates for the container. +// +//# ### Events +// +// `tasklist:enabled` +// +// Fired when the TaskList is enabled. +// +// * **Synchronicity** Sync +// * **Bubbles** Yes +// * **Cancelable** No +// * **Target** `.js-task-list-container` +// +// `tasklist:disabled` +// +// Fired when the TaskList is disabled. +// +// * **Synchronicity** Sync +// * **Bubbles** Yes +// * **Cancelable** No +// * **Target** `.js-task-list-container` +// +// `tasklist:change` +// +// Fired before the TaskList item change takes affect. +// +// * **Synchronicity** Sync +// * **Bubbles** Yes +// * **Cancelable** Yes +// * **Target** `.js-task-list-field` +// +// `tasklist:changed` +// +// Fired once the TaskList item change has taken affect. +// +// * **Synchronicity** Sync +// * **Bubbles** Yes +// * **Cancelable** No +// * **Target** `.js-task-list-field` +// +// ### NOTE +// +// Task list checkboxes are rendered as disabled by default because rendered +// user content is cached without regard for the viewer. (function() { var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; @@ -18,20 +121,48 @@ complete = "[x]"; + // Escapes the String for regular expression matching. escapePattern = function(str) { return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]"); }; - incompletePattern = RegExp("" + (escapePattern(incomplete))); - - completePattern = RegExp("" + (escapePattern(complete))); + incompletePattern = RegExp("" + (escapePattern(incomplete))); // escape square brackets + // match all white space + completePattern = RegExp("" + (escapePattern(complete))); // match all cases + // Pattern used to identify all task list items. + // Useful when you need iterate over all items. itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))"); + // prefix, consisting of + // optional leading whitespace + // zero or more blockquotes + // list item indicator + // optional whitespace prefix + // checkbox + // is followed by whitespace + // is not part of a [foo](url) link + // and is followed by zero or more links + // and either a non-link or the end of the string + // Used to filter out code fences from the source for comparison only. + // http://rubular.com/r/x5EwZVrloI + // Modified slightly due to issues with JS codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg; + // ``` + // followed by optional language + // whitespace + // code + // whitespace + // ``` + // Used to filter out potential mismatches (items not in lists). + // http://rubular.com/r/OInl6CiePy itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g"); + // Given the source text, updates the appropriate task list item to match the + // given checked value. + // + // Returns the updated String text. updateTaskListItem = function(source, itemIndex, checked) { var clean, index, line, result; clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n"); @@ -55,6 +186,9 @@ return result.join("\n"); }; + // Updates the $field value to reflect the state of $item. + // Triggers the `tasklist:change` event before the value has changed, and fires + // a `tasklist:changed` event once the value has changed. updateTaskList = function($item) { var $container, $field, checked, event, index; $container = $item.closest('.js-task-list-container'); @@ -70,10 +204,12 @@ } }; + // When the task list item checkbox is updated, submit the change $(document).on('change', '.task-list-item-checkbox', function() { return updateTaskList($(this)); }); + // Enables TaskList item changes. enableTaskList = function($container) { if ($container.find('.js-task-list-field').length > 0) { $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null); @@ -81,6 +217,7 @@ } }; + // Enables a collection of TaskList containers. enableTaskLists = function($containers) { var container, i, len, results; results = []; @@ -91,11 +228,13 @@ return results; }; + // Disable TaskList item changes. disableTaskList = function($container) { $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled'); return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled'); }; + // Disables a collection of TaskList containers. disableTaskLists = function($containers) { var container, i, len, results; results = []; |