diff options
145 files changed, 3070 insertions, 862 deletions
diff --git a/.eslintignore b/.eslintignore index 453747e14e1..a57137b4d70 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +/coverage-javascript/ /public/ /tmp/ /vendor/ diff --git a/.eslintrc b/.eslintrc index 16eb18ecba2..b58007d90a9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,11 @@ { "extends": "airbnb", + "plugins": [ + "filenames" + ], + "rules": { + "filenames/match-regex": [2, "^[a-z_]+$"] + }, "globals": { "$": false, "_": false, diff --git a/CHANGELOG.md b/CHANGELOG.md index 67dafd4e422..edcfedece5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,42 @@ -Please view this file on the master branch, on stable branches it's out of date. +**Note:** This file is automatically generated. Please see the [developer +documentation](doc/development/changelog.md) for instructions on adding your own +entry. ## 8.14.0 (2016-11-22) + +- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 - Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - Trim leading and trailing whitespace on project_path (Linus Thiel) - Prevent award emoji via notes for issues/MRs authored by user (barthc) +- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - Fix Markdown styling inside reference links (Jan Zdráhal) - Fix extra space on Build sidebar on Firefox !7060 +- Fail gracefully when creating merge request with non-existing branch (alexsanford) - Fix mobile layout issues in admin user overview page !7087 - Fix HipChat notifications rendering (airatshigapov, eisnerd) +- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato) - Refactor Jira service to use jira-ruby gem - Improved todos empty state - Add hover to trash icon in notes !7008 (blackst0ne) +- Hides project activity tabs when features are disabled - Only show one error message for an invalid email !5905 (lycoperdon) +- Added guide describing how to upgrade PostgreSQL using Slony - Fix sidekiq stats in admin area (blackst0ne) +- Added label description as tooltip to issue board list title - Created cycle analytics bundle JavaScript file - API: Fix booleans not recognized as such when using the `to_boolean` helper - Removed delete branch tooltip !6954 - Stop unauthorized users dragging on milestone page (blackst0ne) - Restore issue boards welcome message when a project is created !6899 +- Check that JavaScript file names match convention !7238 (winniehell) - Do not show tooltip for active element !7105 (winniehell) - Escape ref and path for relative links !6050 (winniehell) - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) +- Fix broken issue/merge request links in JIRA comments. !6143 (Brian Kintz) - Fix filtering of milestones with quotes in title (airatshigapov) +- Fix issue boards dragging bug in Safari - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) - Fix showing pipeline status for a given commit from correct branch !7034 - Update mail_room and enable sentinel support to Reply By Email (!7101) @@ -33,24 +46,28 @@ Please view this file on the master branch, on stable branches it's out of date. - New issue board list dropdown stays open after adding a new list - Fix: Backup restore doesn't clear cache - Optimize Event queries by removing default order +- Remove duplicate links from sidebar - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) +- Add Rake task to create/repair GitLab Shell hooks symlinks !5634 - Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld) - Replace jquery.cookie plugin with js.cookie !7085 - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - Show full status link on MR & commit pipelines - Fix documents and comments on Build API `scope` +- Initialize Sidekiq with the list of queues used by GitLab - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - Shortened merge request modal to let clipboard button not overlap +- Adds JavaScript validation for group path editing field - In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo) - Improve search query parameter naming in /admin/users !7115 (YarNayar) +- Fix table pagination to be responsive - Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar) -## 8.13.3 +## 8.13.3 (2016-11-02) -- Fix relative links in Markdown wiki when displayed in "Project" tab !7218 -- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project -- Fix project features default values +- Removes any symlinks before importing a project export file. CVE-2016-9086 +- Fixed Import/Export foreign key issue to do with project members. ## 8.13.2 (2016-10-31) @@ -240,6 +257,11 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix broken Project API docs (Takuya Noguchi) - Migrate invalid project members (owner -> master) +## 8.12.8 (2016-11-02) + +- Removes any symlinks before importing a project export file. CVE-2016-9086 +- Fixed Import/Export foreign key issue to do with project members. + ## 8.12.7 - Prevent running `GfmAutocomplete` setup for each diff note. !6569 @@ -499,6 +521,10 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs +## 8.11.10 (2016-11-02) + +- Removes any symlinks before importing a project export file. CVE-2016-9086 + ## 8.11.9 - Don't send Private-Token (API authentication) headers to Sentry @@ -739,6 +765,10 @@ Please view this file on the master branch, on stable branches it's out of date. - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +## 8.10.13 (2016-11-02) + +- Removes any symlinks before importing a project export file. CVE-2016-9086 + ## 8.10.12 - Don't send Private-Token (API authentication) headers to Sentry diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4635e50c28..67c30c2424c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,6 @@ - [Technical debt](#technical-debt) - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - - [Merge request description format](#merge-request-description-format) - [Contribution acceptance criteria](#contribution-acceptance-criteria) - [Changes for Stable Releases](#changes-for-stable-releases) - [Definition of done](#definition-of-done) @@ -247,13 +246,7 @@ request is as follows: 1. Fork the project into your personal space on GitLab.com 1. Create a feature branch, branch away from `master` 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code -1. Add your changes to the [CHANGELOG.md](CHANGELOG.md): - 1. If you are fixing a ~regression issue, you can add your entry to the next - patch release (e.g. `8.12.5` if current version is `8.12.4`) - 1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if - current version is `8.12.4` - 1. Please add your entry at a random place among the entries of the targeted - release +1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by @@ -262,8 +255,11 @@ request is as follows: 1. Submit a merge request (MR) to the `master` branch 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you - used to achieve it, see the [merge request description format] - (#merge-request-description-format) + used to achieve it. + 1. If you are contributing code, fill in the template already provided in the + "Description" field. + 1. If you are contributing documentation, choose `Documentation` from the + "Choose a template" menu and fill in the template. 1. If the MR changes the UI it should include *Before* and *After* screenshots 1. If the MR changes CSS classes please include the list of affected pages, `grep css-class ./app -R` @@ -469,6 +465,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [contributor-covenant]: http://contributor-covenant.org [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming +[changelog]: doc/development/changelog.md "Generate a changelog entry" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" @@ -117,7 +117,7 @@ gem 'truncato', '~> 0.7.8' gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2' # Diffs -gem 'diffy', '~> 3.0.3' +gem 'diffy', '~> 3.1.0' # Application server group :unicorn do @@ -196,7 +196,7 @@ gem 'loofah', '~> 2.0.3' gem 'licensee', '~> 8.0.0' # Protect against bruteforcing -gem 'rack-attack', '~> 4.3.1' +gem 'rack-attack', '~> 4.4.1' # Ace editor gem 'ace-rails-ap', '~> 4.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 81af1ad2dac..888fa6b2bf5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -180,7 +180,7 @@ GEM railties rotp (~> 2.0) diff-lcs (1.2.5) - diffy (3.0.7) + diffy (3.1.0) docile (1.1.5) doorkeeper (4.2.0) railties (>= 4.2) @@ -520,7 +520,7 @@ GEM rack (1.6.4) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.3.1) + rack-attack (4.4.1) rack rack-cors (0.4.0) rack-mount (0.8.3) @@ -847,7 +847,7 @@ DEPENDENCIES default_value_for (~> 3.0.0) devise (~> 4.2) devise-two-factor (~> 3.0.0) - diffy (~> 3.0.3) + diffy (~> 3.1.0) doorkeeper (~> 4.2.0) dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) @@ -929,7 +929,7 @@ DEPENDENCIES poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) pry-rails (~> 0.3.4) - rack-attack (~> 4.3.1) + rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) rails (= 4.2.7.1) diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index e520170ef74..db9a5a8e40a 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -22,7 +22,7 @@ fallbackClass: 'is-dragging', fallbackOnBody: true, ghostClass: 'is-ghost', - filter: '.has-tooltip, .btn', + filter: '.board-delete, .btn', delay: gl.issueBoards.touchEnabled ? 100 : 50, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ff8b8f6d0ae..8e4fd1f19ba 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -299,7 +299,7 @@ }; Dispatcher.prototype.initFieldErrors = function() { - $('.show-gl-field-errors').each((i, form) => { + $('.gl-show-field-errors').each((i, form) => { new gl.GlFieldErrors(form); }); }; diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6 new file mode 100644 index 00000000000..f7cbecc0385 --- /dev/null +++ b/app/assets/javascripts/gl_field_error.js.es6 @@ -0,0 +1,164 @@ +/* eslint-disable no-param-reassign */ +((global) => { + /* + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + * <form class='gl-show-field-errors'> + * <input type='text' name='username' title='Username is required.'/> + * </form> + * + * Ignore specific inputs (e.g. UsernameValidator): + * + * <form class='gl-show-field-errors'> + * <div class="form-group> + * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/> + * </div> + * <div class="form-group"> + * <input type='text' name='username' title='Username is required.'/> + * </div> + * </form> + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + * <form class='gl-show-field-errors'> + * <div class="form-group gl-field-error-anchor"> + * <input type='text' name='username' title='Username is required.'/> + * // Error message typically injected here + * </div> + * // Error message now injected here + * </form> + * + * */ + + /* + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + * */ + + const errorMessageClass = 'gl-field-error'; + const inputErrorClass = 'gl-field-error-outline'; + const errorAnchorSelector = '.gl-field-error-anchor'; + const ignoreInputSelector = '.gl-field-error-ignore'; + + class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); + } + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } + } + + global.GlFieldError = GlFieldError; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index be6c3ec274f..6ce392d2a5b 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,131 +1,9 @@ /* eslint-disable */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `show-gl-field-errors` to the form element, and ensure error messages are - * declared in each inputs' title attribute. - * - * Example: - * - * <form class='show-gl-field-errors'> - * <input type='text' name='username' title='Username is required.'/> - *</form> - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`); - - this.state = { - valid: false, - empty: true - }; - - this.initFieldValidation(); - } - - initFieldValidation() { - // hidden when injected into DOM - this.inputElement.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } - - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity - const ignoreSelector = '.validation-ignore'; - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } - - renderValidity() { - this.renderClear(); - - if (this.state.valid) { - return this.renderValid(); - } - - if (this.state.empty) { - return this.renderEmpty(); - } - - if (!this.state.valid) { - return this.renderInvalid(); - } - - } +//= require gl_field_error - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.field_validator') - .on('keyup.field_validator', this.updateValidity.bind(this)); - - } - - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } - - getInputValidity() { - return this.inputDomElement.validity.valid; - } - - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } - - renderValid() { - return this.renderClear(); - } - - renderEmpty() { - return this.renderInvalid(); - } - - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } - - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); - } - } - - const customValidationFlag = 'no-gl-field-errors'; +((global) => { + const customValidationFlag = 'gl-field-error-ignore'; class GlFieldErrors { constructor(form) { @@ -144,7 +22,7 @@ this.state.inputs = this.form.find(validateSelectors).toArray() .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new GlFieldError({ input, formErrors: this })); + .map((input) => new global.GlFieldError({ input, formErrors: this })); this.form.on('submit', this.catchInvalidFormSubmit); } diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 6658e4811ce..860ee5df57e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -238,8 +238,11 @@ _this.expandViewContainer(); } _this.diffsLoaded = true; - _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); + var anchoredDiff = gl.utils.getLocationHash(); + if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { + _this.scrollToElement("#diffs"); + _this.highlighSelectedLine(); + }); _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { e.preventDefault(); @@ -252,6 +255,17 @@ }); }; + MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { + var diffTitle = $('#file-path-' + anchoredDiff); + var diffFile = diffTitle.closest('.diff-file'); + var nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + diffFile.singleFileDiff(true, cb); + } else { + cb(); + } + }; + MergeRequestTabs.prototype.highlighSelectedLine = function() { var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; $('.hll').removeClass('hll'); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index adca76ddd5f..8e54ca4f0dc 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -13,7 +13,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file) { + function SingleFileDiff(file, forceLoad, cb) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -32,9 +32,12 @@ this.$toggleIcon.addClass('fa-caret-down'); } $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); + if (forceLoad) { + this.toggleDiff(null, cb); + } } - SingleFileDiff.prototype.toggleDiff = function(e) { + SingleFileDiff.prototype.toggleDiff = function(e, cb) { var $target = $(e.target); if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; @@ -54,11 +57,11 @@ } } else { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - return this.getContentHTML(); + return this.getContentHTML(cb); } }; - SingleFileDiff.prototype.getContentHTML = function() { + SingleFileDiff.prototype.getContentHTML = function(cb) { this.collapsedContent.hide(); this.loadingContent.show(); $.get(this.diffForPath, (function(_this) { @@ -76,6 +79,8 @@ if (typeof DiffNotesApp !== 'undefined') { DiffNotesApp.compileComponents(); } + + if (cb) cb(); }; })(this)); }; @@ -84,10 +89,10 @@ })(); - $.fn.singleFileDiff = function() { + $.fn.singleFileDiff = function(forceLoad, cb) { return this.each(function() { - if (!$.data(this, 'singleFileDiff')) { - return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); + if (!$.data(this, 'singleFileDiff') || forceLoad) { + return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb)); } }); }; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index a18d16ea46c..cfd1e2204d5 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -11,11 +11,9 @@ $this.parent().find('.star-count').text(data.star_count); if (isStarred) { $starSpan.removeClass('starred').text('Star'); - gl.utils.updateTooltipTitle($this, 'Star project'); $starIcon.removeClass('fa-star').addClass('fa-star-o'); } else { $starSpan.addClass('starred').text('Unstar'); - gl.utils.updateTooltipTitle($this, 'Unstar project'); $starIcon.removeClass('fa-star-o').addClass('fa-star'); } }; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 98e301d3799..ce117c3fba5 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -1,11 +1,36 @@ -.avatar { +@mixin avatar-size($size, $margin-right) { + width: $size; + height: $size; + margin-right: $margin-right; +} + +.avatar-container { float: left; - margin-right: 12px; + margin-right: 15px; + border-radius: $avatar_radius; + border: 1px solid rgba(0, 0, 0, .1); + &.s16 { @include avatar-size(16px, 6px); } + &.s20 { @include avatar-size(20px, 7px); } + &.s24 { @include avatar-size(24px, 8px); } + &.s26 { @include avatar-size(26px, 8px); } + &.s32 { @include avatar-size(32px, 10px); } + &.s36 { @include avatar-size(36px, 10px); } + &.s40 { @include avatar-size(40px, 10px); } + &.s46 { @include avatar-size(46px, 15px); } + &.s48 { @include avatar-size(48px, 10px); } + &.s60 { @include avatar-size(60px, 12px); } + &.s70 { @include avatar-size(70px, 14px); } + &.s90 { @include avatar-size(90px, 15px); } + &.s110 { @include avatar-size(110px, 15px); } + &.s140 { @include avatar-size(140px, 15px); } + &.s160 { @include avatar-size(160px, 20px); } +} + +.avatar { + @extend .avatar-container; width: 40px; height: 40px; padding: 0; - border-radius: $avatar_radius; - border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { float: none; @@ -20,22 +45,6 @@ border-radius: 0; border: none; } - - &.s16 { width: 16px; height: 16px; margin-right: 6px; } - &.s20 { width: 20px; height: 20px; margin-right: 7px; } - &.s24 { width: 24px; height: 24px; margin-right: 8px; } - &.s26 { width: 26px; height: 26px; margin-right: 8px; } - &.s32 { width: 32px; height: 32px; margin-right: 10px; } - &.s36 { width: 36px; height: 36px; margin-right: 10px; } - &.s40 { width: 40px; height: 40px; margin-right: 10px; } - &.s46 { width: 46px; height: 46px; margin-right: 15px; } - &.s48 { width: 48px; height: 48px; margin-right: 10px; } - &.s60 { width: 60px; height: 60px; margin-right: 12px; } - &.s70 { width: 70px; height: 70px; margin-right: 14px; } - &.s90 { width: 90px; height: 90px; margin-right: 15px; } - &.s110 { width: 110px; height: 110px; margin-right: 15px; } - &.s140 { width: 140px; height: 140px; margin-right: 20px; } - &.s160 { width: 160px; height: 160px; margin-right: 20px; } } .identicon { @@ -54,3 +63,17 @@ &.s140 { font-size: 72px; line-height: 138px; } &.s160 { font-size: 96px; line-height: 158px; } } + +.image-container { + @extend .avatar-container; + overflow: hidden; + display: flex; + + .avatar { + border-radius: 0; + border: none; + height: auto; + margin: 0; + align-self: center; + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 761c07384f4..f0727e9688a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -136,3 +136,35 @@ label { color: $red-normal; } +.gl-show-field-errors { + .gl-field-success-outline { + border: 1px solid $green-normal; + + &:focus { + box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; + border: 0 none; + } + } + + .gl-field-error-outline { + border: 1px solid $red-normal; + + &:focus { + box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); + border: 0 none; + } + } + + .gl-field-success-message { + color: $green-normal; + } + + .gl-field-error-message { + color: $red-normal; + } + + .gl-field-hint { + color: $gl-text-color; + } +} + diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 3f877d86a26..91ab1503439 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -21,57 +21,66 @@ background: $color-darker; } - .nav-sidebar li { - a { - color: $color-light; - - &:hover, - &:focus, - &:active { - background: $color-dark; - } + .sidebar-header, + .sidebar-action-buttons { + color: $color-light; + background-color: lighten($color-darker, 5%); + } - i { + .nav-sidebar { + li { + a { color: $color-light; - } - - path, - polygon { - fill: $color-light; - } - .count { - color: $color-light; - background: $color-dark; + &:hover, + &:focus, + &:active { + background: $color-dark; + } + + i { + color: $color-light; + } + + path, + polygon { + fill: $color-light; + } + + .count { + color: $color-light; + background: $color-dark; + } + + svg { + position: relative; + top: 3px; + } } - svg { - position: relative; - top: 3px; + &.separate-item { + border-top: 1px solid $color; } - } - - &.separate-item { - border-top: 1px solid $color; - } - &.active a { - color: $white-light; - background: $color-dark; + &.active a { + color: $white-light; + background: $color-dark; - &.no-highlight { - border: none; - } + &.no-highlight { + border: none; + } - i { - color: $white-light; - } + i { + color: $white-light; + } - path, - polygon { - fill: $white-light; + path, + polygon { + fill: $white-light; + } } } + } } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 53ee1ed309e..4993ca7572a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -49,12 +49,16 @@ header { font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; - margin-left: 10px; + margin-left: 8px; height: 28px; min-width: 28px; line-height: 28px; text-align: center; + &.header-user-dropdown-toggle { + margin-left: 14px; + } + &:hover, &:focus, &:active { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 78464af94bd..bc0610cc417 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -142,10 +142,6 @@ ul.content-list { } } - .avatar { - margin-right: 15px; - } - .controls { float: right; diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index b6f21fd8c91..cb2c351c368 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -7,8 +7,70 @@ .pagination { padding: 0; } + + .gap, + .gap:hover { + background-color: $gray-light; + padding: $gl-vert-padding; + cursor: default; + } } .panel > .gl-pagination { margin: 0; } + +/** + * Extra-small screen pagination. + */ +@media (max-width: 320px) { + .gl-pagination { + .first, + .last { + display: none; + } + + .page { + display: none; + + &.active { + display: inline; + } + } + } +} + +/** + * Small screen pagination + */ +@media (max-width: $screen-xs) { + .gl-pagination { + .pagination li a { + padding: 6px 10px; + } + + .page { + display: none; + + &.active { + display: inline; + } + } + } +} + +/** + * Medium screen pagination + */ +@media (min-width: $screen-xs) and (max-width: $screen-md-max) { + .gl-pagination { + .page { + display: none; + + &.active, + &.sibling { + display: inline; + } + } + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c54f7b27575..d74c14ee2a4 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -59,6 +59,11 @@ padding: 0 !important; } + .sidebar-header { + padding: 11px 22px 12px; + font-size: 20px; + } + li { &.separate-item { padding-top: 10px; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ef6833c9845..47a7e84b5c6 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -12,6 +12,10 @@ opacity: 1!important; * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; // !important to make sure no style can override this when dragging cursor: -webkit-grabbing!important; cursor: grabbing!important; @@ -45,11 +49,6 @@ .page-with-sidebar { padding-bottom: 0; } - - .issues-filters { - position: relative; - z-index: 999999; - } } .boards-app { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index d6a55fbd464..6300ac9662f 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -52,10 +52,25 @@ .build-header { position: relative; - padding-right: 40px; + padding: 0; + display: flex; + min-height: 58px; + align-items: center; - @media (min-width: $screen-sm-min) { - padding-right: 0; + .btn-inverted { + @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); + } + + @media (max-width: $screen-sm-max) { + padding-right: 40px; + + .btn-inverted { + display: none; + } + } + + .header-content { + flex: 1; } a { @@ -137,10 +152,15 @@ .retry-link { color: $gl-link-color; + display: none; &:hover { text-decoration: underline; } + + @media (max-width: $screen-sm-max) { + display: block; + } } .stage-item { diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 76225ed8d06..016bab104eb 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -36,10 +36,6 @@ } } -.dash-project-avatar { - float: left; -} - .dash-project-access-icon { float: left; margin-right: 5px; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index ee2a398f031..4375e29c8db 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -3,12 +3,14 @@ } .dashboard .side .panel .panel-heading .input-group { + .form-control { height: 42px; } } .group-row { + .stats { float: right; line-height: $list-text-height; @@ -21,12 +23,14 @@ } .ldap-group-links { + .form-actions { margin-bottom: $gl-padding; } } .groups-cover-block { + .container-fluid { position: relative; } @@ -41,9 +45,14 @@ background-color: $background-color; } } + + .group-avatar { + border: 0; + } } .groups-header { + @media (min-width: $screen-sm-min) { .nav-links { width: 35%; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index a2f5c6c6bd3..10f67b47998 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -75,43 +75,17 @@ .login-body { font-size: 13px; - input + p { margin-top: 5px; } - .gl-field-success-outline { - border: 1px solid $green-normal; - - &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; - border: 0 none; - } - } - - .gl-field-error-outline { - border: 1px solid $red-normal; - - &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); - border: 0 none; - } - } - - .username .validation-success, - .gl-field-success-message { + .username .validation-success { color: $green-normal; } - .username .validation-error, - .gl-field-error-message { + .username .validation-error { color: $red-normal; } - - .gl-field-hint { - color: $gl-text-color; - } - } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f0d39b353d2..f7d54564530 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -96,8 +96,8 @@ .project-avatar { float: none; - margin-left: auto; - margin-right: auto; + margin: 0 auto; + border: none; &.identicon { border-radius: 50%; diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index f8ded05c31a..00e64076408 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -39,6 +39,12 @@ module EventsHelper end end + def event_filter_visible(feature_key) + return true unless @project + + @project.feature_available?(feature_key, current_user) + end + def event_preposition(event) if event.push? || event.commented? || event.target "at" diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5bcf199d468..0a493b7a12b 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -237,7 +237,7 @@ class JiraService < IssueTrackerService end def resource_url(resource) - "#{Settings.gitlab['url'].chomp("/")}#{resource}" + "#{Settings.gitlab.base_url.chomp("/")}#{resource}" end def build_entity_url(entity_name, entity_id) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index fbb3d4507d6..1ee31023e26 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -2,11 +2,11 @@ class ProjectPolicy < BasePolicy def rules team_access!(user) - owner = user.admin? || - project.owner == user || + owner = project.owner == user || (project.group && project.group.has_owner?(user)) - owner_access! if owner + owner_access! if user.admin? || owner + team_member_owner_access! if owner if project.public? || (project.internal? && !user.external?) guest_access! @@ -16,7 +16,7 @@ class ProjectPolicy < BasePolicy can! :read_build if project.public_builds? if project.request_access_enabled && - !(owner || project.team.member?(user) || project_group_member?(user)) + !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access end end @@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy can! :destroy_issue end + def team_member_owner_access! + team_member_reporter_access! + end + # Push abilities on the users team role def team_access!(user) access = project.team.max_member_access(user.id) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 404f75616b5..f415244068b 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -13,20 +13,8 @@ module MergeRequests merge_request.target_project ||= (project.forked_from_project || project) merge_request.target_branch ||= merge_request.target_project.default_branch - if merge_request.target_branch.blank? || merge_request.source_branch.blank? - message = - if params[:source_branch] || params[:target_branch] - "You must select source and target branch" - end - - return build_failed(merge_request, message) - end - - if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch - - return build_failed(merge_request, 'You must select different branches') - end + messages = validate_branches(merge_request) + return build_failed(merge_request, messages) unless messages.empty? compare = CompareService.new.execute( merge_request.source_project, @@ -43,6 +31,34 @@ module MergeRequests private + def validate_branches(merge_request) + messages = [] + + if merge_request.target_branch.blank? || merge_request.source_branch.blank? + messages << + if params[:source_branch] || params[:target_branch] + "You must select source and target branch" + end + end + + if merge_request.source_project == merge_request.target_project && + merge_request.target_branch == merge_request.source_branch + + messages << 'You must select different branches' + end + + # See if source and target branches exist + unless merge_request.source_project.commit(merge_request.source_branch) + messages << "Source branch \"#{merge_request.source_branch}\" does not exist" + end + + unless merge_request.target_project.commit(merge_request.target_branch) + messages << "Target branch \"#{merge_request.target_branch}\" does not exist" + end + + messages + end + # When your branch name starts with an iid followed by a dash this pattern will be # interpreted as the user wants to close that issue on this project. # @@ -91,8 +107,10 @@ module MergeRequests merge_request end - def build_failed(merge_request, message) - merge_request.errors.add(:base, message) unless message.nil? + def build_failed(merge_request, messages) + messages.compact.each do |message| + merge_request.errors.add(:base, message) + end merge_request.compare_commits = [] merge_request.can_be_created = false merge_request diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml index acbe17036f7..1af7dd5bb67 100644 --- a/app/views/admin/appearances/preview.html.haml +++ b/app/views/admin/appearances/preview.html.haml @@ -1,6 +1,6 @@ = render 'devise/shared/tab_single', tab_title: 'Sign in preview' .login-box - %form.show-gl-field-errors + %form.gl-show-field-errors .form-group = label_tag :login = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.' diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index adfa1eaafc9..05c88ca1cc8 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -16,7 +16,8 @@ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) - = image_tag group_icon(group), class: "avatar s40 hidden-xs" + .image-container.s40 + = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do = group.name diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 0188ed448ce..a7c1a4f5038 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -13,7 +13,8 @@ Group info: %ul.well-list %li - = image_tag group_icon(@group), class: "avatar s60" + .image-container.s60 + = image_tag group_icon(@group), class: "avatar s60" %li %span.light Name: %strong= @group.name diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 339cfc613fe..10dce6f3d8f 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -76,7 +76,8 @@ .title = link_to [:admin, project.namespace.becomes(Namespace), project] do .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .image-container.s40 + = project_icon(project, alt: '', class: 'avatar project-avatar s40') %span.project-full-name %span.namespace-name - if project.namespace diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index 5d25dd398d6..73e70dc63e5 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -1,7 +1,7 @@ = render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions' .login-box .login-body - = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f| .devise-errors = devise_error_messages! .form-group diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index b518fae7c95..5e189e6dc54 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,7 +1,7 @@ = render 'devise/shared/tab_single', tab_title:'Change your password' .login-box .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f| .devise-errors = devise_error_messages! = f.hidden_field :reset_password_token diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 1fcfd06419a..99ce13adf74 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -1,7 +1,7 @@ = render 'devise/shared/tab_single', tab_title: 'Reset Password' .login-box .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f| .devise-errors = devise_error_messages! .form-group diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 5fd896f6835..21b89580818 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,4 +1,4 @@ -= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f| += form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| %div.form-group = f.label "Username or email", for: :login = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index 1d381ad7893..a6cadbcbdff 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,4 +1,4 @@ -= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do += form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do .form-group = label_tag :username, 'Username or email' = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index c18bc2ac413..3ab5461f929 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,4 +1,4 @@ -= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do += form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do .form-group = label_tag :username, "#{server['label']} Username" = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index fd77cdbee2e..2cadc424668 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -7,7 +7,7 @@ .login-box .login-body - if @user.two_factor_otp_enabled? - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) %div diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index d0bbcf3115e..7c68e3266e5 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,6 +1,6 @@ #register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } .login-body - = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f| + = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = devise_error_messages! %div.form-group @@ -8,7 +8,7 @@ = f.text_field :name, class: "form-control top", required: true, title: "This field is required." %div.username.form-group = f.label :username - = f.text_field :username, class: "form-control middle no-gl-field-error", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index 49b2f77111f..b2f48a4e0bf 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -1,7 +1,7 @@ = render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions' .login-box .login-body - = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f| .devise-errors = devise_error_messages! .form-group.append-bottom-20 diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index c766370d5a0..2f90c19d4b4 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -2,13 +2,14 @@ .panel-heading Group settings .panel-body - = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| + = form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f| = form_errors(@group) = render 'shared/group_form', f: f .form-group .col-sm-offset-2.col-sm-10 - = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' + .image-container.s160 + = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' %p.light - if @group.avatar? You can change your group avatar here diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 2b8bc269e64..d19eaa6add9 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -5,7 +5,7 @@ New Group %hr -= form_for @group, html: { class: 'group-form form-horizontal' } do |f| += form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f| = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index fab61f447c2..275581b3af8 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,7 +6,8 @@ .cover-block.groups-cover-block %div{ class: container_class } - = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile" + .image-container.s70.group-avatar + = image_tag group_icon(@group), class: "avatar s70 avatar-tile" .group-info .cover-title %h1 diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 80ca30f36e6..889514c4755 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -4,6 +4,6 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "page"} - %span.page.gap +%li + %span.gap = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 522e4d1d05f..750aed8f329 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li{class: "page#{' active' if page.current?}"} +%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"} = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7faa8bded86..7a9859262f7 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -29,10 +29,6 @@ = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_pending_count - - if current_user.can_create_project? - %li - = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -48,6 +44,8 @@ = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } + %li + = link_to "Help", help_path, aria: { label: "Help" } %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 3b1295dc3c0..a0356feef95 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,49 +1,38 @@ -%ul.nav.nav-sidebar - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - %span - Projects - = nav_link(controller: :todos) do - = link_to dashboard_todos_path, title: 'Todos' do - %span - Todos - %span.count.js-todos-count= number_with_delimiter(todos_pending_count) - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - %span - Activity - - if koding_enabled? - = nav_link(controller: :koding) do - = link_to koding_path, title: 'Koding' do +.nav-sidebar + .sidebar-header Across GitLab + %ul.nav + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do %span - Koding - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to dashboard_groups_path, title: 'Groups' do - %span - Groups - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, title: 'Milestones' do - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - %span - Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - %span - Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, title: 'Snippets' do - %span - Snippets - = nav_link(controller: :help) do - = link_to help_path, title: 'Help' do - %span - Help - = nav_link(html_options: {class: profile_tab_class}) do - = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do - %span - Profile Settings + Projects + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + %span + Activity + - if koding_enabled? + = nav_link(controller: :koding) do + = link_to koding_path, title: 'Koding' do + %span + Koding + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to dashboard_groups_path, title: 'Groups' do + %span + Groups + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, title: 'Milestones' do + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + %span + Issues + %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + = nav_link(path: 'dashboard#merge_requests') do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do + %span + Merge Requests + %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, title: 'Snippets' do + %span + Snippets diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index d3987fc9c4f..e67b66d1fff 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,8 @@ - empty_repo = @project.empty_repo? .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } %div{ class: container_class } - = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile') + .image-container.s70.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title = @project.name %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index ba1502c97b6..f7071051efc 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -11,7 +11,9 @@ .board-inner %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } - {{ list.title }} + %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')", + data: { container: "body", placement: "bottom" } } + {{ list.title }} .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } {{ list.issuesSize }} diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 51b5bd9db42..3f2ce7377fd 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,16 +1,19 @@ .content-block.build-header - = ci_status_with_icon(@build.status) - Build - %strong ##{@build.id} - for commit - = link_to ci_status_path(@build.pipeline) do - %strong= @build.pipeline.short_sha - from - = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do - %code - = @build.ref - - if @build.user - = render "user" - = time_ago_with_tooltip(@build.created_at) + .header-content + = ci_status_with_icon(@build.status) + Build + %strong ##{@build.id} + for commit + = link_to ci_status_path(@build.pipeline) do + %strong= @build.pipeline.short_sha + from + = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do + %code + = @build.ref + - if @build.user + = render "user" + = time_ago_with_tooltip(@build.created_at) + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index b1053028279..28f519f11b2 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -44,7 +44,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 retry-link', method: :post + = link_to "Retry build", 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: diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 29d549a60f5..27da86b9efe 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -5,10 +5,10 @@ = custom_icon('icon_fork') %span Fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do = custom_icon('icon_fork') %span Fork %div.count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 311583037e5..12d35101770 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,5 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do + = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do - if current_user.starred?(@project) = icon('star') %span.starred Unstar diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 30473d14b9b..c40ad06969e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -118,7 +118,8 @@ Project avatar .form-group - if @project.avatar? - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + .image-container.s160 + = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } @@ -180,6 +181,7 @@ %ul %li Build traces and artifacts %li LFS objects + %li Container registry images %hr - if can? current_user, :archive_project, @project .row.prepend-top-default diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 6624d5cb427..4e41a15d9f4 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -33,7 +33,12 @@ .form-actions - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel" + .pull-right + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do + Delete + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped" - else = f.submit 'Create page', class: "btn-create btn" - = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" + .pull-right + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 4ea75dbbf0c..763c2fea39b 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -7,6 +7,3 @@ - if can?(current_user, :create_wiki, @project) = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do - Delete diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 233538bb488..679d6018bef 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -19,7 +19,5 @@ - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do New Page - = render 'main_links' - = render 'form' diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 3480800369a..c367ae336db 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,6 +1,9 @@ %ul.nav-links.event-filter.scrolling-tabs = event_filter_link EventFilter.all, 'All' - = event_filter_link EventFilter.push, 'Push events' - = event_filter_link EventFilter.merged, 'Merge events' - = event_filter_link EventFilter.comments, 'Comments' + - if event_filter_visible(:repository) + = event_filter_link EventFilter.push, 'Push events' + - if event_filter_visible(:merge_requests) + = event_filter_link EventFilter.merged, 'Merge events' + - if event_filter_visible(:issues) + = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 67072b9fc2a..ba25e09d638 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -9,11 +9,13 @@ = f.label :path, class: 'control-label' do Group path .col-sm-10 - .input-group + .input-group.gl-field-error-anchor .input-group-addon = root_url = f.text_field :path, placeholder: 'open-source', class: 'form-control', - autofocus: local_assigns[:autofocus] || false + autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+", + required: true, title: 'Please choose a group name with no special characters.' + - if @group.persisted? .alert.alert-warning.prepend-top-10 %ul diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index dc4ee3074d2..562291a61df 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -24,7 +24,8 @@ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) - = image_tag group_icon(group), class: "avatar s40 hidden-xs" + .image-container.s40 + = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do = group.name diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index f27a9002ec2..40fe53e6a8d 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,10 +1,10 @@ - project = @target_project || @project - extra_class = extra_class || '' - show_menu_above = show_menu_above || false -- selected_text = selected.try(:title) +- selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? - = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) + = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e8668048703..3d2122a159c 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -32,10 +32,11 @@ = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar - - if use_creator_avatar - = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - - else - = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .image-container.s40 + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s40') %span.project-full-name %span.namespace-name - if project.namespace && !skip_namespace diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index f360fbb3d5d..78f253f9023 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,4 +1,5 @@ .clearfix - groups.each do |group| = link_to group, class: 'profile-groups-avatars inline', title: group.name do - = image_tag group_icon(group), class: 'avatar group-avatar s40' + .image-container.s40 + = image_tag group_icon(group), class: 'avatar group-avatar s40' diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index eee0ca12af9..2fff6b0105d 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -16,7 +16,7 @@ class PostReceive post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) if post_received.project.nil? - log("Triggered hook for non-existing project with full path \"#{repo_path} \"") + log("Triggered hook for non-existing project with full path \"#{repo_path}\"") return false end @@ -25,7 +25,7 @@ class PostReceive elsif post_received.regular_project? process_project_changes(post_received) else - log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"") + log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"") false end end @@ -37,7 +37,7 @@ class PostReceive @user ||= post_received.identify(newrev) unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier} \"") + log("Triggered hook for non-existing user \"#{post_received.identifier}\"") return false end diff --git a/bin/changelog b/bin/changelog new file mode 100755 index 00000000000..b6586ebb6aa --- /dev/null +++ b/bin/changelog @@ -0,0 +1,170 @@ +#!/usr/bin/env ruby +# +# Generate a changelog entry file in the correct location. +# +# Automatically stages the file and amends the previous commit if the `--amend` +# argument is used. + +require 'optparse' +require 'yaml' + +Options = Struct.new( + :amend, + :author, + :dry_run, + :force, + :merge_request, + :title +) + +class ChangelogOptionParser + def self.parse(argv) + options = Options.new + + parser = OptionParser.new do |opts| + opts.banner = "Usage: #{__FILE__} [options] [title]\n\n" + + # Note: We do not provide a shorthand for this in order to match the `git + # commit` interface + opts.on('--amend', 'Amend the previous commit') do |value| + options.amend = value + end + + opts.on('-f', '--force', 'Overwrite an existing entry') do |value| + options.force = value + end + + opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value| + options.merge_request = value + end + + opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value| + options.dry_run = value + end + + opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value| + options.author = git_user_name if value + end + + opts.on('-h', '--help', 'Print help message') do + $stdout.puts opts + exit + end + end + + parser.parse!(argv) + + # Title is everything that remains, but let's clean it up a bit + options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '') + + options + end + + def self.git_user_name + %x{git config user.name}.strip + end +end + +class ChangelogEntry + attr_reader :options + + def initialize(options) + @options = options + + assert_feature_branch! + assert_new_file! + assert_title! + + $stdout.puts "\e[32mcreate\e[0m #{file_path}" + $stdout.puts contents + + unless options.dry_run + write + amend_commit if options.amend + end + end + + def contents + YAML.dump( + 'title' => title, + 'merge_request' => options.merge_request, + 'author' => options.author + ) + end + + def write + File.write(file_path, contents) + end + + def amend_commit + %x{git add #{file_path}} + exec("git commit --amend") + end + + private + + def fail_with(message) + $stderr.puts "\e[31merror\e[0m #{message}" + exit 1 + end + + def assert_feature_branch! + return unless branch_name == 'master' + + fail_with "Create a branch first!" + end + + def assert_new_file! + return unless File.exist?(file_path) + return if options.force + + fail_with "#{file_path} already exists! Use `--force` to overwrite." + end + + def assert_title! + return if options.title.length > 0 || options.amend + + fail_with "Provide a title for the changelog entry or use `--amend`" \ + " to use the title from the previous commit." + end + + def title + if options.title.empty? + last_commit_subject + else + options.title + end + end + + def last_commit_subject + %x{git log --format="%s" -1}.strip + end + + def file_path + File.join( + unreleased_path, + branch_name.gsub(/[^\w-]/, '-') << '.yml' + ) + end + + def unreleased_path + File.join('changelogs', 'unreleased').tap do |path| + path << '-ee' if ee? + end + end + + def ee? + @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__)) + end + + def branch_name + @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '') + end +end + +if $0 == __FILE__ + options = ChangelogOptionParser.parse(ARGV) + ChangelogEntry.new(options) +end + +# vim: ft=ruby diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 3451b68cea5..699ab6075b6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -432,7 +432,9 @@ production: &base ## Repositories settings repositories: # Paths where repositories can be stored. Give the canonicalized absolute pathname. - # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!! + # IMPORTANT: None of the path components may be symlink, because + # gitlab-shell invokes Dir.pwd inside the repository path and that results + # real path not the symlink. storages: # You must have at least a `default` storage path. default: /home/git/repositories/ diff --git a/config/initializers/0_post_deployment_migrations.rb b/config/initializers/0_post_deployment_migrations.rb new file mode 100644 index 00000000000..0068a03d214 --- /dev/null +++ b/config/initializers/0_post_deployment_migrations.rb @@ -0,0 +1,12 @@ +# Post deployment migrations are included by default. This file must be loaded +# before other initializers as Rails may otherwise memoize a list of migrations +# excluding the post deployment migrations. +unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] + path = Rails.root.join('db', 'post_migrate').to_s + + Rails.application.config.paths['db/migrate'] << path + + # Rails memoizes migrations at certain points where it won't read the above + # path just yet. As such we must also update the following list of paths. + ActiveRecord::Migrator.migrations_paths << path +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f7e714cd6bc..0455a98dbfe 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -42,3 +42,19 @@ end Sidekiq.configure_client do |config| config.redis = redis_config_hash end + +# The Sidekiq client API always adds the queue to the Sidekiq queue +# list, but mail_room and gitlab-shell do not. This is only necessary +# for monitoring. +config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s) + +begin + Sidekiq.redis do |conn| + conn.pipelined do + config[:queues].each do |queue| + conn.sadd('queues', queue[0]) + end + end + end +rescue Redis::BaseError, SocketError +end diff --git a/db/post_migrate/.gitkeep b/db/post_migrate/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/db/post_migrate/.gitkeep diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md new file mode 100644 index 00000000000..f3c2e72341f --- /dev/null +++ b/doc/administration/raketasks/maintenance.md @@ -0,0 +1,220 @@ +# Maintenance Rake Tasks + +## Gather information about GitLab and the system it runs on + +This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:env:info +``` + +**Source Installation** + +``` +bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +Example output: + +``` +System information +System: Debian 7.8 +Current User: git +Using RVM: no +Ruby Version: 2.1.5p273 +Gem Version: 2.4.3 +Bundler Version: 1.7.6 +Rake Version: 10.3.2 +Sidekiq Version: 2.17.8 + +GitLab information +Version: 7.7.1 +Revision: 41ab9e1 +Directory: /home/git/gitlab +DB Adapter: postgresql +URL: https://gitlab.example.com +HTTP Clone URL: https://gitlab.example.com/some-project.git +SSH Clone URL: git@gitlab.example.com:some-project.git +Using LDAP: no +Using Omniauth: no + +GitLab Shell +Version: 2.4.1 +Repositories: /home/git/repositories/ +Hooks: /home/git/gitlab-shell/hooks/ +Git: /usr/bin/git +``` + +## Check GitLab configuration + +Runs the following rake tasks: + +- `gitlab:gitlab_shell:check` +- `gitlab:sidekiq:check` +- `gitlab:app:check` + +It will check that each component was setup according to the installation guide and suggest fixes for issues found. + +You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide). + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:check +``` + +**Source Installation** + +``` +bundle exec rake gitlab:check RAILS_ENV=production +``` + +NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output. + +Example output: + +``` +Checking Environment ... + +Git configured for git user? ... yes +Has python2? ... yes +python2 is supported version? ... yes + +Checking Environment ... Finished + +Checking GitLab Shell ... + +GitLab Shell version? ... OK (1.2.0) +Repo base directory exists? ... yes +Repo base directory is a symlink? ... no +Repo base owned by git:git? ... yes +Repo base access is drwxrws---? ... yes +post-receive hook up-to-date? ... yes +post-receive hooks in repos are links: ... yes + +Checking GitLab Shell ... Finished + +Checking Sidekiq ... + +Running? ... yes + +Checking Sidekiq ... Finished + +Checking GitLab ... + +Database config exists? ... yes +Database is SQLite ... no +All migrations up? ... yes +GitLab config exists? ... yes +GitLab config outdated? ... no +Log directory writable? ... yes +Tmp directory writable? ... yes +Init script exists? ... yes +Init script up-to-date? ... yes +Redis version >= 2.0.0? ... yes + +Checking GitLab ... Finished +``` + +## Rebuild authorized_keys file + +In some case it is necessary to rebuild the `authorized_keys` file. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:shell:setup +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production +``` + +``` +This will rebuild an authorized_keys file. +You will lose any data stored in authorized_keys file. +Do you want to continue (yes/no)? yes +``` + +## Clear redis cache + +If for some reason the dashboard shows wrong information you might want to +clear Redis' cache. + +**Omnibus Installation** + +``` +sudo gitlab-rake cache:clear +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +## Precompile the assets + +Sometimes during version upgrades you might end up with some wrong CSS or +missing some icons. In that case, try to precompile the assets again. + +Note that this only applies to source installations and does NOT apply to +Omnibus packages. + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production +``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. + +## Tracking Deployments + +GitLab provides a Rake task that lets you track deployments in GitLab +Performance Monitoring. This Rake task simply stores the current GitLab version +in the GitLab Performance Monitoring database. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:track_deployment +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production +``` + +## Create or repair repository hooks symlink + +If the GitLab shell hooks directory location changes or another circumstance +leads to the hooks symlink becoming missing or invalid, run this Rake task +to create or repair the symlinks. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:shell:create_hooks +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production +``` diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index d8dce4388e1..6f1356ddf8f 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section. 1. To see the current threads, run: ``` - apply all thread bt + thread apply all bt ``` 1. Once you're done debugging with `gdb`, be sure to detach from the process and exit: diff --git a/doc/api/projects.md b/doc/api/projects.md index 8ebac57e612..4f4b20a1874 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1139,6 +1139,7 @@ Parameters: | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | +| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | ### Edit project hook @@ -1164,6 +1165,7 @@ Parameters: | `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `wiki_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | +| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | ### Delete project hook diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 073e99b7147..efd23d514bc 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -27,11 +27,14 @@ Example response: ```json [ - { - "id" : 1, - "url" : "https://gitlab.example.com/hook", - "created_at" : "2015-11-04T20:07:35.874Z" - } + { + "id":1, + "url":"https://gitlab.example.com/hook", + "created_at":"2016-10-31T12:32:15.192Z", + "push_events":true, + "tag_push_events":false, + "enable_ssl_verification":true + } ] ``` @@ -48,6 +51,10 @@ POST /hooks | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `url` | string | yes | The hook URL | +| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | +| `push_events` | boolean | no | When true, the hook will fire on push events | +| `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed | +| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | Example request: @@ -59,11 +66,14 @@ Example response: ```json [ - { - "id" : 2, - "url" : "https://gitlab.example.com/hook", - "created_at" : "2015-11-04T20:07:35.874Z" - } + { + "id":1, + "url":"https://gitlab.example.com/hook", + "created_at":"2016-10-31T12:32:15.192Z", + "push_events":true, + "tag_push_events":false, + "enable_ssl_verification":true + } ] ``` diff --git a/doc/development/README.md b/doc/development/README.md index 14d6f08e43a..bf1f054b7d5 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -21,6 +21,7 @@ ## Process +- [Generate a changelog entry with `bin/changelog`](changelog.md) - [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 @@ -41,6 +42,7 @@ - [What requires downtime?](what_requires_downtime.md) - [Adding database indexes](adding_database_indexes.md) +- [Post Deployment Migrations](post_deployment_migrations.md) ## Compliance diff --git a/doc/development/changelog.md b/doc/development/changelog.md new file mode 100644 index 00000000000..6a97fae9cac --- /dev/null +++ b/doc/development/changelog.md @@ -0,0 +1,185 @@ +# Generate a changelog entry + +This guide contains instructions for generating a changelog entry data file, as +well as information and history about our changelog process. + +## Overview + +Each bullet point, or **entry**, in our [`CHANGELOG.md`][changelog.md] file is +generated from a single data file in the [`changelogs/unreleased/`][unreleased] +(or corresponding EE) folder. The file is expected to be a [YAML] file in the +following format: + +```yaml +--- +title: "Going through change[log]s" +merge_request: 1972 +author: Ozzy Osbourne +``` + +The `merge_request` value is a reference to a merge request that adds this +entry, and the `author` key is used to give attribution to community +contributors. Both are optional. + +Community contributors and core team members are encouraged to add their name to +the `author` field. GitLab team members should not. + +If you're working on the GitLab EE repository, the entry will be added to +`changelogs/unreleased-ee/` instead. + +[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md +[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/ +[YAML]: https://en.wikipedia.org/wiki/YAML + +## Instructions + +A `bin/changelog` script is available to generate the changelog entry file +automatically. + +Its simplest usage is to provide the value for `title`: + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' +create changelogs/unreleased/my-feature.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: +author: +``` + +The entry filename is based on the name of the current Git branch. If you run +the command above on a branch called `feature/hey-dz`, it will generate a +`changelogs/unreleased/feature-hey-dz.yml` file. + +### Arguments + +| Argument | Shorthand | Purpose | +| ----------------- | --------- | --------------------------------------------- | +| `--amend` | | Amend the previous commit | +| `--force` | `-f` | Overwrite an existing entry | +| `--merge-request` | `-m` | Merge Request ID | +| `--dry-run` | `-n` | Don't actually write anything, just print | +| `--git-username` | `-u` | Use Git user.name configuration as the author | +| `--help` | `-h` | Print help message | + +#### `--amend` + +You can pass the **`--amend`** argument to automatically stage the generated +file and amend it to the previous commit. + +If you use **`--amend`** and don't provide a title, it will automatically use +the "subject" of the previous commit, which is the first line of the commit +message: + +```text +$ git show --oneline +ab88683 Added an awesome new feature to GitLab + +$ bin/changelog --amend +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Added an awesome new feature to GitLab +merge_request: +author: +``` + +#### `--force` or `-f` + +Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it +already exists. + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' +error changelogs/unreleased/feature-hey-dz.yml already exists! Use `--force` to overwrite. + +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' --force +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: 1983 +author: +``` + +#### `--merge-request` or `-m` + +Use the **`--merge-request`** or **`-m`** argument to provide the +`merge_request` value: + +```text +$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983 +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: 1983 +author: +``` + +#### `--dry-run` or `-n` + +Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or +committing anything: + +```text +$ bin/changelog --amend --dry-run +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Added an awesome new feature to GitLab +merge_request: +author: + +$ ls changelogs/unreleased/ +``` + +#### `--git-username` or `-u` + +Use the **`--git-username`** or **`-u`** argument to automatically fill in the +`author` value with your configured Git `user.name` value: + +```text +$ git config user.name +Jane Doe + +$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!' +create changelogs/unreleased/feature-hey-dz.yml +--- +title: Hey DZ, I added a feature to GitLab! +merge_request: +author: Jane Doe +``` + +## History and Reasoning + +Our `CHANGELOG` file was previously updated manually by each contributor that +felt their change warranted an entry. When two merge requests added their own +entries at the same spot in the list, it created a merge conflict in one as soon +as the other was merged. When we had dozens of merge requests fighting for the +same changelog entry location, this quickly became a major source of merge +conflicts and delays in development. + +This led us to a [boring solution] of "add your entry in a random location in +the list." This actually worked pretty well as we got further along in each +monthly release cycle, but at the start of a new cycle, when a new version +section was added and there were fewer places to "randomly" add an entry, the +conflicts became a problem again until we had a sufficient number of entries. + +On top of all this, it created an entirely different headache for [release managers] +when they cherry-picked a commit into a stable branch for a patch release. If +the commit included an entry in the `CHANGELOG`, it would include the entire +changelog for the latest version in `master`, so the release manager would have +to manually remove the later entries. They often would have had to do this +multiple times per patch release. This was compounded when we had to release +multiple patches at once due to a security issue. + +We needed to automate all of this manual work. So we [started brainstorming]. +After much discussion we settled on the current solution of one file per entry, +and then compiling the entries into the overall `CHANGELOG.md` file during the +[release process]. + +[boring solution]: https://about.gitlab.com/handbook/#boring-solutions +[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md +[started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826 +[release process]: https://gitlab.com/gitlab-org/release-tools + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/post_deployment_migrations.md b/doc/development/post_deployment_migrations.md new file mode 100644 index 00000000000..cfc91539bee --- /dev/null +++ b/doc/development/post_deployment_migrations.md @@ -0,0 +1,75 @@ +# Post Deployment Migrations + +Post deployment migrations are regular Rails migrations that can optionally be +executed after a deployment. By default these migrations are executed alongside +the other migrations. To skip these migrations you will have to set the +environment variable `SKIP_POST_DEPLOYMENT_MIGRATIONS` to a non-empty value +when running `rake db:migrate`. + +For example, this would run all migrations including any post deployment +migrations: + +```bash +bundle exec rake db:migrate +``` + +This however will skip post deployment migrations: + +```bash +SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rake db:migrate +``` + +## Deployment Integration + +Say you're using Chef for deploying new versions of GitLab and you'd like to run +post deployment migrations after deploying a new version. Let's assume you +normally use the command `chef-client` to do so. To make use of this feature +you'd have to run this command as follows: + +```bash +SKIP_POST_DEPLOYMENT_MIGRATIONS=true sudo chef-client +``` + +Once all servers have been updated you can run `chef-client` again on a single +server _without_ the environment variable. + +The process is similar for other deployment techniques: first you would deploy +with the environment variable set, then you'll essentially re-deploy a single +server but with the variable _unset_. + +## Creating Migrations + +To create a post deployment migration you can use the following Rails generator: + +```bash +bundle exec rails g post_deployment_migration migration_name_here +``` + +This will generate the migration file in `db/post_migrate`. These migrations +behave exactly like regular Rails migrations. + +## Use Cases + +Post deployment migrations can be used to perform migrations that mutate state +that an existing version of GitLab depends on. For example, say you want to +remove a column from a table. This requires downtime as a GitLab instance +depends on this column being present while it's running. Normally you'd follow +these steps in such a case: + +1. Stop the GitLab instance +2. Run the migration removing the column +3. Start the GitLab instance again + +Using post deployment migrations we can instead follow these steps: + +1. Deploy a new version of GitLab while ignoring post deployment migrations +2. Re-run `rake db:migrate` but without the environment variable set + +Here we don't need any downtime as the migration takes place _after_ a new +version (which doesn't depend on the column anymore) has been deployed. + +Some other examples where these migrations are useful: + +* Cleaning up data generated due to a bug in GitLab +* Removing tables +* Migrating jobs from one Sidekiq queue to another diff --git a/doc/development/testing.md b/doc/development/testing.md index 8e91ac5e3ba..b0b26ccf57a 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires no more than one new `step` definition. If more than that is required, the test should be re-implemented using RSpec instead. +## Testing Rake Tasks + +To make testing Rake tasks a little easier, there is a helper that can be included +in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use +`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures +a few other things to make testing Rake tasks easier. + +At a minimum, requiring the Rake helper will redirect `stdout`, include the +runtime task helpers, and include the `RakeHelpers` Spec support module. + +The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make +executing tasks simple. See `spec/support/rake_helpers.rb` for all available +methods. + +Example: + +```ruby +require 'rake_helper' + +describe 'gitlab:shell rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/shell' + + stub_warn_user_is_not_gitlab + end + + describe 'install task' do + it 'invokes create_hooks task' do + expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) + + run_rake_task('gitlab:shell:install') + end + end +end +``` + --- [Return to Development documentation](README.md) diff --git a/doc/install/installation.md b/doc/install/installation.md index 795e1d23443..83090f46271 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -271,9 +271,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-13-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab -**Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/integration/jira.md b/doc/integration/jira.md index cf1557ddc44..2e31fd994de 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -135,7 +135,7 @@ password as they will be needed when configuring GitLab in the next section. JIRA configuration in GitLab is done via a project's **Services**. -#### GitLab 13.0 with JIRA v1000.x +#### GitLab 8.13.0 with JIRA v1000.x To enable JIRA integration in a project, navigate to the project's and open the context menu clicking on the top right gear icon, then go to @@ -160,7 +160,7 @@ with the linked JIRA project. #### GitLab 6.x-7.7 with JIRA v6.x -_**Note:** GitLab versions 13.0 and up contain various integration improvements. +_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. We strongly recommend upgrading._ In `gitlab.yml` enable the JIRA issue tracker section by diff --git a/doc/integration/saml.md b/doc/integration/saml.md index f3b2a288776..4a242c321aa 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro the SAML request, but this error never reaches GitLab due to the CSRF check. To bypass this you can add `skip_before_action :verify_authenticity_token` to the -`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, -where it can then be seen in the usual logs, or as a flash message in the login -screen. - -That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` -for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for -installations from source. +`omniauth_callbacks_controller.rb` file immediately after the `class` line and +comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This +will allow the error to hit GitLab, where it can then be seen in the usual logs, +or as a flash message on the login screen. + +That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` +for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for +installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn` +command on Omnibus installations and `sudo service gitlab restart` on installations +from source. + +You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer) +(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace) +(Chrome) browser extensions useful in your debugging. ### Invalid audience diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 315cb56a089..266aeb7d60e 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -1,188 +1,3 @@ -# Maintenance +# Maintenance Rake Tasks -## Gather information about GitLab and the system it runs on - -This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues. - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:env:info - -# installation from source -bundle exec rake gitlab:env:info RAILS_ENV=production -``` - -Example output: - -``` -System information -System: Debian 7.8 -Current User: git -Using RVM: no -Ruby Version: 2.1.5p273 -Gem Version: 2.4.3 -Bundler Version: 1.7.6 -Rake Version: 10.3.2 -Sidekiq Version: 2.17.8 - -GitLab information -Version: 7.7.1 -Revision: 41ab9e1 -Directory: /home/git/gitlab -DB Adapter: postgresql -URL: https://gitlab.example.com -HTTP Clone URL: https://gitlab.example.com/some-project.git -SSH Clone URL: git@gitlab.example.com:some-project.git -Using LDAP: no -Using Omniauth: no - -GitLab Shell -Version: 2.4.1 -Repositories: /home/git/repositories/ -Hooks: /home/git/gitlab-shell/hooks/ -Git: /usr/bin/git -``` - -## Check GitLab configuration - -Runs the following rake tasks: - -- `gitlab:gitlab_shell:check` -- `gitlab:sidekiq:check` -- `gitlab:app:check` - -It will check that each component was setup according to the installation guide and suggest fixes for issues found. - -You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide). - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:check - -# installation from source -bundle exec rake gitlab:check RAILS_ENV=production -``` - -NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output. - -Example output: - -``` -Checking Environment ... - -Git configured for git user? ... yes -Has python2? ... yes -python2 is supported version? ... yes - -Checking Environment ... Finished - -Checking GitLab Shell ... - -GitLab Shell version? ... OK (1.2.0) -Repo base directory exists? ... yes -Repo base directory is a symlink? ... no -Repo base owned by git:git? ... yes -Repo base access is drwxrws---? ... yes -post-receive hook up-to-date? ... yes -post-receive hooks in repos are links: ... yes - -Checking GitLab Shell ... Finished - -Checking Sidekiq ... - -Running? ... yes - -Checking Sidekiq ... Finished - -Checking GitLab ... - -Database config exists? ... yes -Database is SQLite ... no -All migrations up? ... yes -GitLab config exists? ... yes -GitLab config outdated? ... no -Log directory writable? ... yes -Tmp directory writable? ... yes -Init script exists? ... yes -Init script up-to-date? ... yes -Redis version >= 2.0.0? ... yes - -Checking GitLab ... Finished -``` - -## Rebuild authorized_keys file - -In some case it is necessary to rebuild the `authorized_keys` file. - -For Omnibus-packages: -``` -sudo gitlab-rake gitlab:shell:setup -``` - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production -``` - -``` -This will rebuild an authorized_keys file. -You will lose any data stored in authorized_keys file. -Do you want to continue (yes/no)? yes -``` - -## Clear redis cache - -If for some reason the dashboard shows wrong information you might want to -clear Redis' cache. - -For Omnibus-packages: -``` -sudo gitlab-rake cache:clear -``` - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production -``` - -## Precompile the assets - -Sometimes during version upgrades you might end up with some wrong CSS or -missing some icons. In that case, try to precompile the assets again. - -Note that this only applies to source installations and does NOT apply to -omnibus packages. - -For installations from source: -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production -``` - -For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at -the release of upstream GitLab. The omnibus version includes optimized versions -of those assets. Unless you are modifying the JavaScript / CSS code on your -production machine after installing the package, there should be no reason to redo -rake assets:precompile on the production machine. If you suspect that assets -have been corrupted, you should reinstall the omnibus package. - -## Tracking Deployments - -GitLab provides a Rake task that lets you track deployments in GitLab -Performance Monitoring. This Rake task simply stores the current GitLab version -in the GitLab Performance Monitoring database. - -For Omnibus-packages: - -``` -sudo gitlab-rake gitlab:track_deployment -``` - -For installations from source: - -``` -cd /home/git/gitlab -sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production -``` +This document was moved to [administration/raketasks/maintenance](../administration/raketasks/maintenance.md). diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md new file mode 100644 index 00000000000..787511fd6cf --- /dev/null +++ b/doc/update/8.13-to-8.14.md @@ -0,0 +1,205 @@ +# From 8.13 to 8.14 + +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 + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +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-14-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-14-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 v4.0.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.5 +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-13-stable:config/gitlab.yml.example origin/8-14-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-13-stable:lib/support/nginx/gitlab-ssl origin/8-14-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-14-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-14-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-14-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 + +For Ubuntu 16.04.1 LTS: + + sudo systemctl daemon-reload + +### 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.13) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.12 to 8.13](8.12-to-8.13.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/README.md b/doc/update/README.md index 975d72164b4..837b31abb97 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -85,6 +85,8 @@ possible. - [MySQL installation guide](../install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) +- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for + upgrading a PostgreSQL database with minimal downtime. [omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md new file mode 100644 index 00000000000..f009906256e --- /dev/null +++ b/doc/update/upgrading_postgresql_using_slony.md @@ -0,0 +1,482 @@ +# Upgrading PostgreSQL Using Slony + +This guide describes the steps one can take to upgrade their PostgreSQL database +to the latest version without the need for hours of downtime. This guide assumes +you have two database servers: one database server running an older version of +PostgreSQL (e.g. 9.2.18) and one server running a newer version (e.g. 9.6.0). + +For this process we'll use a PostgreSQL replication tool called +["Slony"](http://www.slony.info/). Slony allows replication between different +PostgreSQL versions and as such can be used to upgrade a cluster with a minimal +amount of downtime. + +In various places we'll refer to the user `gitlab-psql`. This user should be the +user used to run the various PostgreSQL OS processes. If you're using a +different user (e.g. `postgres`) you should replace `gitlab-psql` with the name +of said user. This guide also assumes your database is called +`gitlabhq_production`. If you happen to use a different database name you should +change this accordingly. + +## Database Dumps + +Slony only replicates data and not any schema changes. As a result we must +ensure that all databases have the same database structure. + +To do so we'll generate a dump of our current database. This dump will only +contain the structure, not any data. To generate this dump run the following +command on your active database server: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql -p 5432 -U gitlab-psql -s -f /tmp/structure.sql gitlabhq_production +``` + +If you're not using GitLab's Omnibus package you may have to adjust the paths to +`pg_dump` and the PostgreSQL installation directory to match the paths of your +configuration. + +Once the structure dump is generated we also need to generate a dump for the +`schema_migrations` table. This table doesn't have any primary keys and as such +can't be replicated easily by Slony. To generate this dump run the following +command on your active database server: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -a -t schema_migrations -f /tmp/migrations.sql gitlabhq_production +``` + +Next we'll need to move these files somewhere accessible by the new database +server. The easiest way is to simply download these files to your local system: + +```bash +scp your-user@production-database-host:/tmp/*.sql /tmp +``` + +This will copy all the SQL files located in `/tmp` to your local system's +`/tmp` directory. Once copied you can safely remove the files from the database +server. + +## Installing Slony + +Slony will be used to upgrade the database without requiring long downtimes. +Slony can be downloaded from http://www.slony.info/. If you have installed +PostgreSQL using your operating system's package manager you may also be able to +install Slony using said package manager. + +When compiling Slony from source you *must* use the following commands to do so: + +```bash +./configure --prefix=/path/to/installation/directory --with-perltools --with-pgconfigdir=/path/to/directory/containing/pg_config/bin +make +make install +``` + +Omnibus users can use the following commands: + +```bash +./configure --prefix=/opt/gitlab/embedded --with-perltools --with-pgconfigdir=/opt/gitlab/embedded/bin +make +make install +``` + +This assumes you have installed GitLab into /opt/gitlab. + +To test if Slony is installed properly, run the following commands: + +```bash +test -f /opt/gitlab/embedded/bin/slonik && echo 'Slony installed' || echo 'Slony not installed' +test -f /opt/gitlab/embedded/bin/slonik_init_cluster && echo 'Slony Perl tools are available' || echo 'Slony Perl tools are not available' +/opt/gitlab/embedded/bin/slonik -v +``` + +This assumes Slony was installed to `/opt/gitlab/embedded`. If Slony was +installed properly the output of these commands will be (the mentioned "slonik" +version may be different): + +``` +Slony installed +Slony Perl tools are available +slonik version 2.2.5 +``` + +## Slony User + +Next we must set up a PostgreSQL user that Slony can use to replicate your +database. To do so, log in to your production database using `psql` using a +super user account. Once done run the following SQL queries: + +```sql +CREATE ROLE slony WITH SUPERUSER LOGIN REPLICATION ENCRYPTED PASSWORD 'password string here'; +ALTER ROLE slony SET statement_timeout TO 0; +``` + +Make sure you replace "password string here" with the actual password for the +user. A password is *required*. This user must be created on _both_ the old and +new database server using the same password. + +Once the user has been created make sure you note down the password as we will +need it later on. + +## Configuring Slony + +Now we can finally start configuring Slony. Slony uses a configuration file for +most of the work so we'll need to set this one up. This configuration file +specifies where to put log files, how Slony should connect to the databases, +etc. + +First we'll need to create some required directories and set the correct +permissions. To do so, run the following commands on both the old and new +database server: + +```bash +sudo mkdir -p /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony +sudo chown gitlab-psql:root /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony +``` + +Here `gitlab-psql` is the user used to run the PostgreSQL database processes. If +you're using a different user you should replace this with the name of said +user. + +Now that the directories are in place we can create the configuration file. For +this we can use the following template: + +```perl +if ($ENV{"SLONYNODES"}) { + require $ENV{"SLONYNODES"}; +} else { + $CLUSTER_NAME = 'slony_replication'; + $LOGDIR = '/var/log/gitlab/slony'; + $MASTERNODE = 1; + $DEBUGLEVEL = 2; + + add_node(host => 'OLD_HOST', dbname => 'gitlabhq_production', port =>5432, + user=>'slony', password=>'SLONY_PASSWORD', node=>1); + + add_node(host => 'NEW_HOST', dbname => 'gitlabhq_production', port =>5432, + user=>'slony', password=>'SLONY_PASSWORD', node=>2, parent=>1 ); +} + +$SLONY_SETS = { + "set1" => { + "set_id" => 1, + "table_id" => 1, + "sequence_id" => 1, + "pkeyedtables" => [ + TABLES + ], + }, +}; + +if ($ENV{"SLONYSET"}) { + require $ENV{"SLONYSET"}; +} + +# Please do not add or change anything below this point. +1; +``` + +In this configuration file you should replace a few placeholders before you can +use it. The following placeholders should be replaced: + +* `OLD_HOST`: the address of the old database server. +* `NEW_HOST`: the address of the new database server. +* `SLONY_PASSWORD`: the password of the Slony user created earlier. +* `TABLES`: the tables to replicate. + +The list of tables to replicate can be generated by running the following +command on your old PostgreSQL database: + +``` +sudo gitlab-psql gitlabhq_production -c "select concat('\"', schemaname, '.', tablename, '\",') from pg_catalog.pg_tables where schemaname = 'public' and tableowner = 'gitlab' and tablename != 'schema_migrations' order by tablename asc;" -t +``` + +If you're not using Omnibus you should replace `gitlab-psql` with the +appropriate path to the `psql` executable. + +The above command outputs a list of tables in a format that can be copy-pasted +directly into the above configuration file. Make sure to _replace_ `TABLES` with +this output, don't just append it below it. Once done you'll end up with +something like this: + +```perl +"pkeyedtables" => [ + "public.abuse_reports", + "public.appearances", + "public.application_settings", + ... more rows here ... +] +``` + +Once you have the configuration file generated you must install it on both the +old and new database. To do so, place it in +`/var/opt/gitlab/postgresql/slony/slon_tools.conf` (for which we created the +directory earlier on). + +Now that the configuration file is in place we can _finally_ start replicating +our database. First we must set up the schema in our new database. To do so make +sure that the SQL files we generated earlier can be found in the `/tmp` +directory of the new server. Once these files are in place start a `psql` +session on this server: + +``` +sudo gitlab-psql gitlabhq_production +``` + +Now run the following commands: + +``` +\i /tmp/structure.sql +\i /tmp/migrations.sql +``` + +To verify if the structure is in place close the session, start it again, then +run `\d`. If all went well you should see output along the lines of the +following: + +``` + List of relations + Schema | Name | Type | Owner +--------+---------------------------------------------+----------+------------- + public | abuse_reports | table | gitlab + public | abuse_reports_id_seq | sequence | gitlab + public | appearances | table | gitlab + public | appearances_id_seq | sequence | gitlab + public | application_settings | table | gitlab + public | application_settings_id_seq | sequence | gitlab + public | approvals | table | gitlab + ... more rows here ... +``` + +Now we can initialize the required tables and what not that Slony will use for +its replication process. To do so, run the following on the old database: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_init_cluster --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +If all went well this will produce something along the lines of: + +``` +<stdin>:10: Set up replication nodes +<stdin>:13: Next: configure paths for each node/origin +<stdin>:16: Replication nodes prepared +<stdin>:17: Please start a slon replication daemon for each node +``` + +Next we need to start a replication node on every server. To do so, run the +following on the old database: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +If all went well this will produce output such as: + + +``` +Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2 slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 & +Slon successfully started for cluster slony_replication, node node1 +PID [26740] +Start the watchdog process as well... +``` + +Next we need to run the following command on the _new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +This will produce similar output if all went well. + +Next we need to tell the new database server what it should replicate. This can +be done by running the following command on the _new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_create_set 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +This should produce output along the lines of the following: + +``` +<stdin>:11: Subscription set 1 (set1) created +<stdin>:12: Adding tables to the subscription set +<stdin>:16: Add primary keyed table public.abuse_reports +<stdin>:20: Add primary keyed table public.appearances +<stdin>:24: Add primary keyed table public.application_settings +... more rows here ... +<stdin>:327: Adding sequences to the subscription set +<stdin>:328: All tables added +``` + +Finally we can start the replication process by running the following on the +_new_ database server: + +``` +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_subscribe_set 1 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik +``` + +This should produce the following output: + +``` +<stdin>:6: Subscribed nodes to set 1 +``` + +At this point the new database server will start replicating the data of the old +database server. This process can take anywhere from a few minutes to hours, if +not days. Unfortunately Slony itself doesn't really provide a way of knowing +when the two databases are in sync. To get an estimate of the progress you can +use the following shell script: + +``` +#!/usr/bin/env bash + +set -e + +user='slony' +pass='SLONY_PASSWORD' + +function main { + while : + do + local source + local target + + source=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h OLD_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A) + target=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h NEW_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A) + + echo "$(date): ${target} of ${source}" >> progress.log + echo "$(date): ${target} of ${source}" + + sleep 60 + done +} + +main +``` + +This script will compare the sizes of the old and new database every minute and +print the result to STDOUT as well as logging it to a file. Make sure to replace +`SLONY_PASSWORD`, `OLD_HOST`, and `NEW_HOST` with the correct values. + +## Stopping Replication + +At some point the two databases are in sync. Once this is the case you'll need +to plan for a few minutes of downtime. This small downtime window is used to +stop the replication process, remove any Slony data from both databases, restart +GitLab so it can use the new database, etc. + +First, let's stop all of GitLab. Omnibus users can do so by running the +following on their GitLab server(s): + +``` +sudo gitlab-ctl stop unicorn +sudo gitlab-ctl stop sidekiq +sudo gitlab-ctl stop mailroom +``` + +If you have any other processes that use PostgreSQL you should also stop those. + +Once everything has been stopped you should update any configuration settings, +DNS records, etc so they all point to the new database. + +Once the settings have been taken care of we need to stop the replication +process. It's crucial that no new data is written to the databases at this point +as this data will be lost. + +To stop replication, run the following on both database servers: + +```bash +sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_kill --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf +``` + +This will stop all the Slony processes on the host the command was executed on. + +## Resetting Sequences + +The above setup does not replicate database sequences, as such these must be +reset manually in the target database. You can use the following script for +this: + +```bash +#!/usr/bin/env bash +set -e + +function main { + local fix_sequences + local fix_owners + + fix_sequences='/tmp/fix_sequences.sql' + fix_owners='/tmp/fix_owners.sql' + + # The SQL queries were taken from + # https://wiki.postgresql.org/wiki/Fixing_Sequences + sudo gitlab-psql gitlabhq_production -t -c " + SELECT 'ALTER SEQUENCE '|| quote_ident(MIN(schema_name)) ||'.'|| quote_ident(MIN(seq_name)) + ||' OWNED BY '|| quote_ident(MIN(TABLE_NAME)) ||'.'|| quote_ident(MIN(column_name)) ||';' + FROM ( + SELECT + n.nspname AS schema_name, + c.relname AS TABLE_NAME, + a.attname AS column_name, + SUBSTRING(d.adsrc FROM E'^nextval\\(''([^'']*)''(?:::text|::regclass)?\\)') AS seq_name + FROM pg_class c + JOIN pg_attribute a ON (c.oid=a.attrelid) + JOIN pg_attrdef d ON (a.attrelid=d.adrelid AND a.attnum=d.adnum) + JOIN pg_namespace n ON (c.relnamespace=n.oid) + WHERE has_schema_privilege(n.oid,'USAGE') + AND n.nspname NOT LIKE 'pg!_%' escape '!' + AND has_table_privilege(c.oid,'SELECT') + AND (NOT a.attisdropped) + AND d.adsrc ~ '^nextval' + ) seq + GROUP BY seq_name HAVING COUNT(*)=1; + " > "${fix_owners}" + + sudo gitlab-psql gitlabhq_production -t -c " + SELECT 'SELECT SETVAL(' || + quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) || + ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' || + quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';' + FROM pg_class AS S, + pg_depend AS D, + pg_class AS T, + pg_attribute AS C, + pg_tables AS PGT + WHERE S.relkind = 'S' + AND S.oid = D.objid + AND D.refobjid = T.oid + AND D.refobjid = C.attrelid + AND D.refobjsubid = C.attnum + AND T.relname = PGT.tablename + ORDER BY S.relname; + " > "${fix_sequences}" + + sudo gitlab-psql gitlabhq_production -f "${fix_owners}" + sudo gitlab-psql gitlabhq_production -f "${fix_sequences}" + + rm "${fix_owners}" "${fix_sequences}" +} + +main +``` + +Upload this script to the _target_ server and execute it as follows: + +```bash +bash path/to/the/script/above.sh +``` + +This will correct the ownership of sequences and reset the next value for the +`id` column to the next available value. + +## Removing Slony + +Next we need to remove all Slony related data. To do so, run the following +command on the _target_ server: + +```bash +sudo gitlab-psql gitlabhq_production -c "DROP SCHEMA _slony_replication CASCADE;" +``` + +Once done you can safely remove any Slony related files (e.g. the log +directory), and uninstall Slony if desired. At this point you can start your +GitLab instance again and if all went well it should be using your new database +server. diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature index 08b87808f33..bd883a0ebfa 100644 --- a/features/dashboard/active_tab.feature +++ b/features/dashboard/active_tab.feature @@ -18,7 +18,7 @@ Feature: Dashboard Active Tab Then the active main tab should be Merge Requests And no other main tabs should be active - Scenario: On Dashboard Help - Given I visit dashboard help page - Then the active main tab should be Help + Scenario: On Dashboard Groups + Given I visit dashboard groups page + Then the active main tab should be Groups And no other main tabs should be active diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index b1d5e4a7acb..92061dac7f4 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -11,7 +11,6 @@ Feature: Dashboard And I visit dashboard page Scenario: I should see projects list - Then I should see "New Project" link Then I should see "Shop" project link Then I should see "Shop" project CI status diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index 9c94dc70df0..3c5bf44c538 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I visit the "Rake Tasks" help page' do - visit help_page_path("raketasks/maintenance") + visit help_page_path("administration/raketasks/maintenance") end step 'I should see "Rake Tasks" page markdown rendered' do diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb index 5c47238777f..07fff16e867 100644 --- a/features/steps/shared/sidebar_active_tab.rb +++ b/features/steps/shared/sidebar_active_tab.rb @@ -1,12 +1,8 @@ module SharedSidebarActiveTab include Spinach::DSL - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') - end - step 'no other main tabs should be active' do - expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + expect(page).to have_selector('.nav-sidebar li.active', count: 1) end def ensure_active_main_tab(content) @@ -17,6 +13,10 @@ module SharedSidebarActiveTab ensure_active_main_tab('Projects') end + step 'the active main tab should be Groups' do + ensure_active_main_tab('Groups') + end + step 'the active main tab should be Projects' do ensure_active_main_tab('Projects') end @@ -28,8 +28,4 @@ module SharedSidebarActiveTab step 'the active main tab should be Merge Requests' do ensure_active_main_tab('Merge Requests') end - - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') - end end diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb new file mode 100644 index 00000000000..1a7b8d5bf35 --- /dev/null +++ b/generator_templates/rails/post_deployment_migration/migration.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # 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 + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index ab9d2d54f4b..d52496451a2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -43,14 +43,13 @@ module API end class Hook < Grape::Entity - expose :id, :url, :created_at + expose :id, :url, :created_at, :push_events, :tag_push_events + expose :enable_ssl_verification end class ProjectHook < Hook - expose :project_id, :push_events - expose :issues_events, :merge_requests_events, :tag_push_events + expose :project_id, :issues_events, :merge_requests_events expose :note_events, :build_events, :pipeline_events, :wiki_page_events - expose :enable_ssl_verification end class BasicProjectDetails < Grape::Entity diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 14f5be3b5f6..dd93a85dc54 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -47,7 +47,8 @@ module API :build_events, :pipeline_events, :wiki_page_events, - :enable_ssl_verification + :enable_ssl_verification, + :token ] @hook = user_project.hooks.new(attrs) @@ -82,7 +83,8 @@ module API :build_events, :pipeline_events, :wiki_page_events, - :enable_ssl_verification + :enable_ssl_verification, + :token ] if @hook.update_attributes attrs diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 794e34874f4..32f731c5652 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -12,6 +12,7 @@ module API end get do hooks = SystemHook.all + present hooks, with: Entities::Hook end @@ -19,10 +20,14 @@ module API success Entities::Hook end params do - requires :url, type: String, desc: 'The URL for the system hook' + requires :url, type: String, desc: "The URL to send the request to" + optional :token, type: String, desc: 'The token used to validate payloads' + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" end post do - hook = SystemHook.new declared(params).to_h + hook = SystemHook.new declared(params, include_missing: false).to_h if hook.save present hook, with: Entities::Hook diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb new file mode 100644 index 00000000000..7cb4bccb23c --- /dev/null +++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb @@ -0,0 +1,15 @@ +require 'rails/generators' + +module Rails + class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase + def create_migration_file + timestamp = Time.now.strftime('%Y%m%d%H%I%S') + + template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb" + end + + def migration_class_name + file_name.camelize + end + end +end diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb index f755a404693..34169319b26 100644 --- a/lib/gitlab/import_export/attribute_cleaner.rb +++ b/lib/gitlab/import_export/attribute_cleaner.rb @@ -3,10 +3,25 @@ module Gitlab class AttributeCleaner ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] - def self.clean!(relation_hash:) - relation_hash.reject! do |key, _value| - key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key) - end + def self.clean(*args) + new(*args).clean + end + + def initialize(relation_hash:, relation_class:) + @relation_hash = relation_hash + @relation_class = relation_class + end + + def clean + @relation_hash.reject do |key, _value| + prohibited_key?(key) || !@relation_class.attribute_method?(key) + end.except('id') + end + + private + + def prohibited_key?(key) + key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key) end end end diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 113895ba22c..ffd17118c91 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -43,6 +43,14 @@ module Gitlab raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result + remove_symlinks! + end + + def remove_symlinks! + Dir["#{@shared.export_path}/**/*"].each do |path| + FileUtils.rm(path) if File.lstat(path).symlink? + end + true end end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 36c4cf6efa0..b790733f4a7 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -55,7 +55,12 @@ module Gitlab end def member_hash(member) - member.except('id').merge(source_id: @project.id, importing: true) + parsed_hash(member).merge('source_id' => @project.id, 'importing' => true) + end + + def parsed_hash(member) + Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys, + relation_class: ProjectMember) end def find_project_user_query(member) diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 7cdba880a93..c551321c18d 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -9,8 +9,14 @@ module Gitlab end def restore - json = IO.read(@path) - @tree_hash = ActiveSupport::JSON.decode(json) + begin + json = IO.read(@path) + @tree_hash = ActiveSupport::JSON.decode(json) + rescue => e + Rails.logger.error("Import/Export error: #{e.message}") + raise Gitlab::ImportExport::Error.new('Incorrect JSON format') + end + @project_members = @tree_hash.delete('project_members') ActiveRecord::Base.no_touching do diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index dc630e76411..a0e80fccad9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -14,7 +14,7 @@ module Gitlab priorities: :label_priorities, label: :project_label }.freeze - USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze + USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze @@ -30,7 +30,7 @@ module Gitlab def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:) @relation_name = OVERRIDES[relation_sym] || relation_sym - @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id) + @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id) @members_mapper = members_mapper @user = user @imported_object_retries = 0 @@ -172,11 +172,8 @@ module Gitlab end def parsed_relation_hash - @parsed_relation_hash ||= begin - Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash) - - @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) } - end + @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash, + relation_class: relation_class) end def set_st_diffs diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb index fc08082fc86..bd3c3ee3b2f 100644 --- a/lib/gitlab/import_export/version_checker.rb +++ b/lib/gitlab/import_export/version_checker.rb @@ -24,12 +24,19 @@ module Gitlab end def verify_version!(version) - if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version) + if different_version?(version) raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}") else true end end + + def different_version?(version) + Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version) + rescue => e + Rails.logger.error("Import/Export error: #{e.message}") + raise Gitlab::ImportExport::Error.new('Incorrect VERSION format') + end end end end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 210899882b4..58761a129d4 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -63,11 +63,11 @@ namespace :gitlab do # Launch installation process system(*%W(bin/install) + repository_storage_paths_args) - - # (Re)create hooks - system(*%W(bin/create-hooks) + repository_storage_paths_args) end + # (Re)create hooks + Rake::Task['gitlab:shell:create_hooks'].invoke + # Required for debian packaging with PKGR: Setup .ssh/environment with # the current PATH, so that the correct ruby version gets loaded # Requires to set "PermitUserEnvironment yes" in sshd config (should not @@ -102,6 +102,15 @@ namespace :gitlab do end end end + + desc 'Create or repair repository hooks symlink' + task create_hooks: :environment do + warn_user_is_not_gitlab + + puts 'Creating/Repairing hooks symlinks for all repositories' + system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args) + puts 'done'.color(:green) + end end def setup diff --git a/package.json b/package.json index d440307bd10..a303c9c1eac 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "devDependencies": { "eslint": "^3.1.1", "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.0.1", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.4.1" diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb new file mode 100644 index 00000000000..8c8bc1b0f1c --- /dev/null +++ b/spec/bin/changelog_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +load File.expand_path('../../bin/changelog', __dir__) + +describe 'bin/changelog' do + describe ChangelogOptionParser do + it 'parses --ammend' do + options = described_class.parse(%w[foo bar --amend]) + + expect(options.amend).to eq true + end + + it 'parses --force' do + options = described_class.parse(%w[foo --force bar]) + + expect(options.force).to eq true + end + + it 'parses -f' do + options = described_class.parse(%w[foo -f bar]) + + expect(options.force).to eq true + end + + it 'parses --merge-request' do + options = described_class.parse(%w[foo --merge-request 1234 bar]) + + expect(options.merge_request).to eq 1234 + end + + it 'parses -m' do + options = described_class.parse(%w[foo -m 4321 bar]) + + expect(options.merge_request).to eq 4321 + end + + it 'parses --dry-run' do + options = described_class.parse(%w[foo --dry-run bar]) + + expect(options.dry_run).to eq true + end + + it 'parses -n' do + options = described_class.parse(%w[foo -n bar]) + + expect(options.dry_run).to eq true + end + + it 'parses --git-username' do + allow(described_class).to receive(:git_user_name).and_return('Jane Doe') + options = described_class.parse(%w[foo --git-username bar]) + + expect(options.author).to eq 'Jane Doe' + end + + it 'parses -u' do + allow(described_class).to receive(:git_user_name).and_return('John Smith') + options = described_class.parse(%w[foo -u bar]) + + expect(options.author).to eq 'John Smith' + end + + it 'parses -h' do + expect do + $stdout = StringIO.new + + described_class.parse(%w[foo -h bar]) + end.to raise_error(SystemExit) + end + + it 'assigns title' do + options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend]) + + expect(options.title).to eq 'foo bar baz' + end + end +end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 3234fabe288..a92075fec8f 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -53,7 +53,7 @@ describe 'Issue Boards', feature: true, js: true do context 'with lists' do let(:milestone) { create(:milestone, project: project) } - let(:planning) { create(:label, project: project, name: 'Planning') } + let(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') } let(:development) { create(:label, project: project, name: 'Development') } let(:testing) { create(:label, project: project, name: 'Testing') } let(:bug) { create(:label, project: project, name: 'Bug') } @@ -91,6 +91,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.board', count: 4) end + it 'shows description tooltip on list title' do + page.within('.board:nth-child(2)') do + expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') + end + end + it 'shows issues in lists' do wait_for_board_cards(2, 2) wait_for_board_cards(3, 2) diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 88e1549a22b..9dfa5d1de19 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::None.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') expect(page).to have_css('.issue', count: 1) end @@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end @@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 1) end @@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end end @@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) expect(page).to have_css('.issue', count: 1) end @@ -70,6 +75,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) expect(page).to have_css('.issue', count: 1) end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index b963d1305b5..c68e1ea4af9 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_css('a.btn.active', text: 'Side-by-side') end end + + it 'does not allow non-existing branches' do + visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + + expect(page).to have_content('The form contains the following errors') + expect(page).to have_content('Source branch "non-exist-source" does not exist') + expect(page).to have_content('Target branch "non-exist-target" does not exist') + end end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index d1685f95503..63a23a14f20 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -216,7 +216,9 @@ describe "Builds" do @build.run! visit namespace_project_build_path(@project.namespace, @project, @build) click_link 'Cancel' - click_link 'Retry' + page.within('.build-header') do + click_link 'Retry build' + end end it 'shows the right status and buttons' do diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index d25cf7fb353..e796ee570b7 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -164,5 +164,23 @@ describe 'Edit Project Settings', feature: true do expect(page).to have_content "Customize your workflow!" end + + it "hides project activity tabs" do + select "Disabled", from: "project_project_feature_attributes_repository_access_level" + select "Disabled", from: "project_project_feature_attributes_issues_access_level" + select "Disabled", from: "project_project_feature_attributes_wiki_access_level" + + click_button "Save changes" + wait_for_ajax + + visit activity_namespace_project_path(project.namespace, project) + + page.within(".event-filter") do + expect(page).to have_selector("a", count: 2) + expect(page).not_to have_content("Push events") + expect(page).not_to have_content("Merge events") + expect(page).not_to have_content("Comments") + end + end end end diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml index 2526e5e33a5..69445b61367 100644 --- a/spec/javascripts/fixtures/gl_field_errors.html.haml +++ b/spec/javascripts/fixtures/gl_field_errors.html.haml @@ -1,4 +1,4 @@ -%form.show-gl-field-errors{action: 'submit', method: 'post'} +%form.gl-show-field-errors{action: 'submit', method: 'post'} .form-group %input.required-text{required: true, type: 'text'} Text .form-group @@ -10,6 +10,6 @@ .form-group %input.hidden{ type:'hidden' } .form-group - %input.custom.no-gl-field-errors{ type:'text' } Custom, do not validate + %input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate .form-group %input.submit{type: 'submit'} Submit diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 4bdd72800ea..0713e30e485 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -8,7 +8,7 @@ describe('GL Style Field Errors', function() { beforeEach(function() { fixture.load('gl_field_errors.html'); - const $form = this.$form = $('form.show-gl-field-errors'); + const $form = this.$form = $('form.gl-show-field-errors'); this.fieldErrors = new global.GlFieldErrors($form); }); @@ -21,7 +21,7 @@ }); it('should ignore elements with custom error handling', function() { - const customErrorFlag = 'no-gl-field-errors'; + const customErrorFlag = 'gl-field-error-ignore'; const customErrorElem = $(`.${customErrorFlag}`); expect(customErrorElem.length).toBe(1); diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index a5aa387f4f7..62aa212f1f6 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -122,6 +122,14 @@ describe Gitlab::GitAccess, lib: true do describe 'build authentication_abilities permissions' do let(:authentication_abilities) { build_authentication_abilities } + describe 'owner' do + let(:project) { create(:project, namespace: user.namespace) } + + context 'pull code' do + it { expect(subject).to be_allowed } + end + end + describe 'reporter user' do before { project.team << [user, :reporter] } diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index b8e7932eb4a..63bab0f0d0d 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::ImportExport::AttributeCleaner, lib: true do + let(:relation_class){ double('relation_class').as_null_object } let(:unsafe_hash) do { + 'id' => 101, 'service_id' => 99, 'moved_to_id' => 99, 'namespace_id' => 99, @@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do end it 'removes unwanted attributes from the hash' do - described_class.clean!(relation_hash: unsafe_hash) + # allow(relation_class).to receive(:attribute_method?).and_return(true) + parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class) - expect(unsafe_hash).to eq(post_safe_hash) + expect(parsed_hash).to eq(post_safe_hash) end end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb new file mode 100644 index 00000000000..a88ddd17aca --- /dev/null +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::FileImporter, lib: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" } + let(:valid_file) { "#{shared.export_path}/valid.json" } + let(:symlink_file) { "#{shared.export_path}/invalid.json" } + let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" } + + before do + stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) + + setup_files + + described_class.import(archive_file: '', shared: shared) + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + + def setup_files + FileUtils.mkdir_p("#{shared.export_path}/subfolder/") + FileUtils.touch(valid_file) + FileUtils.ln_s(valid_file, symlink_file) + FileUtils.ln_s(valid_file, subfolder_symlink_file) + end +end 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 069ea960321..3038ab53ad8 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do @@ -175,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) end end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original + + restored_project_json + + expect(shared.errors.first).not_to include('test') + end + end + end end end end diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index c680e712b59..2405ac5abfe 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' +include ImportExport::CommonUtil describe Gitlab::ImportExport::VersionChecker, services: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } + describe 'bundle a project Git repo' do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } let(:version) { Gitlab::ImportExport.version } before do @@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do end end end + + describe 'version file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'VERSION') + + described_class.check!(shared: shared) + + expect(shared.errors.first).not_to include('test') + end + end + end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index a9f637147d1..ee0e38bd373 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe JiraService, models: true do + include Gitlab::Routing.url_helpers + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -66,6 +68,29 @@ describe JiraService, models: true do ).once end + it "references the GitLab commit/merge request" do + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ + ).once + end + + it "references the GitLab commit/merge request (relative URL)" do + stub_config_setting(relative_url_root: '/gitlab') + stub_config_setting(url: Settings.send(:build_gitlab_url)) + + allow(JiraService).to receive(:default_url_options) do + { script_name: '/gitlab' } + end + + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ + ).once + end + it "calls the api with jira_issue_transition_id" do @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 658e3c13a73..96249a7d8c3 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -6,6 +6,7 @@ describe ProjectPolicy, models: true do let(:dev) { create(:user) } let(:master) { create(:user) } let(:owner) { create(:user) } + let(:admin) { create(:admin) } let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:guest_permissions) do @@ -155,6 +156,19 @@ describe ProjectPolicy, models: true do it do is_expected.to include(*guest_permissions) is_expected.to include(*reporter_permissions) + is_expected.to include(*team_member_reporter_permissions) + is_expected.to include(*developer_permissions) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(*guest_permissions) + is_expected.to include(*reporter_permissions) is_expected.not_to include(*team_member_reporter_permissions) is_expected.to include(*developer_permissions) is_expected.to include(*master_permissions) diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb new file mode 100644 index 00000000000..9b5b4bf9fea --- /dev/null +++ b/spec/rake_helper.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'rake' + +RSpec.configure do |config| + config.include RakeHelpers + + # Redirect stdout so specs don't have so much noise + config.before(:all) do + $stdout = StringIO.new + + Rake.application.rake_require 'tasks/gitlab/task_helpers' + Rake::Task.define_task :environment + end + + # Reset stdout + config.after(:all) do + $stdout = STDOUT + end +end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index cfcdcad74cd..5f39329a1b8 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -88,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do expect do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true end.to change {project.hooks.count}.by(1) + expect(response).to have_http_status(201) expect(json_response['url']).to eq('http://example.com') expect(json_response['issues_events']).to eq(true) @@ -99,6 +100,24 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(false) expect(json_response['enable_ssl_verification']).to eq(true) + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token + end.to change {project.hooks.count}.by(1) + + expect(response).to have_http_status(201) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = project.hooks.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) end it "returns a 400 error if url not given" do @@ -129,6 +148,19 @@ describe API::API, 'ProjectHooks', api: true do expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end + it "adds the token without including it in the response" do + token = "secret token" + + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token + + expect(response).to have_http_status(200) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + it "returns 404 error if hook id not found" do put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' expect(response).to have_http_status(404) diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index f8a1aed5441..f685a3685e6 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -13,6 +13,7 @@ describe API::API, api: true do context "when no user" do it "returns authentication error" do get api("/hooks") + expect(response).to have_http_status(401) end end @@ -20,6 +21,7 @@ describe API::API, api: true do context "when not an admin" do it "returns forbidden error" do get api("/hooks", user) + expect(response).to have_http_status(403) end end @@ -27,9 +29,12 @@ describe API::API, api: true do context "when authenticated as admin" do it "returns an array of hooks" do get api("/hooks", admin) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['url']).to eq(hook.url) + expect(json_response.first['push_events']).to be true + expect(json_response.first['tag_push_events']).to be false end end end @@ -43,6 +48,7 @@ describe API::API, api: true do it "responds with 400 if url not given" do post api("/hooks", admin) + expect(response).to have_http_status(400) end @@ -51,6 +57,14 @@ describe API::API, api: true do post api("/hooks", admin) end.not_to change { SystemHook.count } end + + it 'sets default values for events' do + post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true + + expect(response).to have_http_status(201) + expect(json_response['enable_ssl_verification']).to be true + expect(json_response['tag_push_events']).to be false + end end describe "GET /hooks/:id" do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index dbdf83a0dff..9bfc84c7425 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -284,7 +284,17 @@ describe 'Git LFS API and storage' do let(:authorization) { authorize_ci_project } shared_examples 'can download LFS only from own projects' do - context 'for own project' do + context 'for owned project' do + let(:project) { create(:empty_project, namespace: user.namespace) } + + let(:update_permissions) do + project.lfs_objects << lfs_object + end + + it_behaves_like 'responds with a file' + end + + context 'for member of project' do let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:update_permissions) do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index c64df4979b0..bb26513103d 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -245,6 +245,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'a pullable' end + + context 'when you are owner' do + let(:project) { create(:empty_project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + end end context 'for private' do @@ -266,6 +272,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'a pullable' end + + context 'when you are owner' do + let(:project) { create(:empty_project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + end end end end @@ -276,13 +288,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'disallow for all' do - let(:project) { create(:empty_project, :public) } + context 'when you are member' do + let(:project) { create(:empty_project, :public) } - before do - project.team << [current_user, :developer] + before do + project.team << [current_user, :developer] + end + + it_behaves_like 'an inaccessible' end - it_behaves_like 'an inaccessible' + context 'when you are owner' do + let(:project) { create(:empty_project, :public, namespace: current_user.namespace) } + + it_behaves_like 'an inaccessible' + end end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3a3f07ddcb9..3f5df049ea2 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do before do allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) + allow(project).to receive(:commit).and_return(commit_1) + allow(project).to receive(:commit).and_return(commit_2) end describe 'execute' do @@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do end end end + + context 'source branch does not exist' do + before do + allow(project).to receive(:commit).with(source_branch).and_return(nil) + allow(project).to receive(:commit).with(target_branch).and_return(commit_1) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist') + end + end + + context 'target branch does not exist' do + before do + allow(project).to receive(:commit).with(source_branch).and_return(commit_1) + allow(project).to receive(:commit).with(target_branch).and_return(nil) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist') + end + end + + context 'both source and target branches do not exist' do + before do + allow(project).to receive(:commit).and_return(nil) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds both error messages to the merge request' do + expect(merge_request.errors).to contain_exactly( + 'Source branch "feature-branch" does not exist', + 'Target branch "master" does not exist' + ) + end + end end end diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb new file mode 100644 index 00000000000..2542a59bb00 --- /dev/null +++ b/spec/support/import_export/common_util.rb @@ -0,0 +1,10 @@ +module ImportExport + module CommonUtil + def setup_symlink(tmpdir, symlink_name) + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir) + + File.open("#{tmpdir}/test", 'w') { |file| file.write("test") } + FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}") + end + end +end diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb new file mode 100644 index 00000000000..52d80c69835 --- /dev/null +++ b/spec/support/rake_helpers.rb @@ -0,0 +1,10 @@ +module RakeHelpers + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end + + def stub_warn_user_is_not_gitlab + allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab) + end +end diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb new file mode 100644 index 00000000000..226d34fe2c9 --- /dev/null +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -0,0 +1,26 @@ +require 'rake_helper' + +describe 'gitlab:shell rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/shell' + + stub_warn_user_is_not_gitlab + end + + describe 'install task' do + it 'invokes create_hooks task' do + expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) + + run_rake_task('gitlab:shell:install') + end + end + + describe 'create_hooks task' do + it 'calls gitlab-shell bin/create_hooks' do + expect_any_instance_of(Object).to receive(:system) + .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args) + + run_rake_task('gitlab:shell:create_hooks') + end + end +end diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js index eca7c5012b2..f9e57bcb855 100644 --- a/vendor/assets/javascripts/Sortable.js +++ b/vendor/assets/javascripts/Sortable.js @@ -4,8 +4,7 @@ * @license MIT */ - -(function (factory) { +(function sortableModule(factory) { "use strict"; if (typeof define === "function" && define.amd) { @@ -15,15 +14,22 @@ module.exports = factory(); } else if (typeof Package !== "undefined") { + //noinspection JSUnresolvedVariable Sortable = factory(); // export for Meteor.js } else { /* jshint sub:true */ window["Sortable"] = factory(); } -})(function () { +})(function sortableFactory() { "use strict"; + if (typeof window == "undefined" || !window.document) { + return function sortableError() { + throw new Error("Sortable.js requires a window with a document"); + }; + } + var dragEl, parentEl, ghostEl, @@ -33,6 +39,7 @@ scrollEl, scrollParentEl, + scrollCustomFn, lastEl, lastCSS, @@ -42,6 +49,8 @@ newIndex, activeGroup, + putSortable, + autoScroll = {}, tapEvt, @@ -58,8 +67,15 @@ document = win.document, parseInt = win.parseInt, + $ = win.jQuery || win.Zepto, + Polymer = win.Polymer, + supportDraggable = !!('draggable' in document.createElement('div')), supportCssPointerEvents = (function (el) { + // false when IE11 + if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) { + return false; + } el = document.createElement('x'); el.style.cssText = 'pointer-events:auto'; return el.style.pointerEvents === 'auto'; @@ -88,13 +104,17 @@ winHeight = window.innerHeight, vx, - vy + vy, + + scrollOffsetX, + scrollOffsetY ; // Delect scrollEl if (scrollParentEl !== rootEl) { scrollEl = options.scroll; scrollParentEl = rootEl; + scrollCustomFn = options.scrollFn; if (scrollEl === true) { scrollEl = rootEl; @@ -136,11 +156,18 @@ if (el) { autoScroll.pid = setInterval(function () { + scrollOffsetY = vy ? vy * speed : 0; + scrollOffsetX = vx ? vx * speed : 0; + + if ('function' === typeof(scrollCustomFn)) { + return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt); + } + if (el === win) { - win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); + win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY); } else { - vy && (el.scrollTop += vy * speed); - vx && (el.scrollLeft += vx * speed); + el.scrollTop += scrollOffsetY; + el.scrollLeft += scrollOffsetX; } }, 24); } @@ -149,19 +176,39 @@ }, 30), _prepareGroup = function (options) { - var group = options.group; + function toFn(value, pull) { + if (value === void 0 || value === true) { + value = group.name; + } - if (!group || typeof group != 'object') { - group = options.group = {name: group}; + if (typeof value === 'function') { + return value; + } else { + return function (to, from) { + var fromGroup = from.options.group.name; + + return pull + ? value + : value && (value.join + ? value.indexOf(fromGroup) > -1 + : (fromGroup == value) + ); + }; + } } - ['pull', 'put'].forEach(function (key) { - if (!(key in group)) { - group[key] = true; - } - }); + var group = {}; + var originalGroup = options.group; + + if (!originalGroup || typeof originalGroup != 'object') { + originalGroup = {name: originalGroup}; + } - options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + + options.group = group; } ; @@ -198,6 +245,7 @@ draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', ignore: 'a, img', filter: null, animation: 0, @@ -211,7 +259,8 @@ forceFallback: false, fallbackClass: 'sortable-fallback', fallbackOnBody: false, - fallbackTolerance: 0 + fallbackTolerance: 0, + fallbackOffset: {x: 0, y: 0} }; @@ -224,7 +273,7 @@ // Bind all private methods for (var fn in this) { - if (fn.charAt(0) === '_') { + if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { this[fn] = this[fn].bind(this); } } @@ -258,7 +307,7 @@ type = evt.type, touch = evt.touches && evt.touches[0], target = (touch || evt).target, - originalTarget = target, + originalTarget = evt.target.shadowRoot && evt.path[0] || target, filter = options.filter, startIndex; @@ -271,13 +320,13 @@ return; // only left button or enabled } - target = _closest(target, options.draggable, el); - - if (!target) { + if (options.handle && !_closest(originalTarget, options.handle, el)) { return; } - if (options.handle && !_closest(originalTarget, options.handle, el)) { + target = _closest(target, options.draggable, el); + + if (!target) { return; } @@ -332,16 +381,18 @@ this._lastX = (touch || evt).clientX; this._lastY = (touch || evt).clientY; + dragEl.style['will-change'] = 'transform'; + dragStartFn = function () { // Delayed drag has been triggered // we can re-enable the events: touchmove/mousemove _this._disableDelayedDrag(); // Make the element draggable - dragEl.draggable = true; + dragEl.draggable = _this.nativeDraggable; // Chosen item - _toggleClass(dragEl, _this.options.chosenClass, true); + _toggleClass(dragEl, options.chosenClass, true); // Bind the events: dragstart/dragend _this._triggerDragStart(touch); @@ -408,7 +459,10 @@ try { if (document.selection) { - document.selection.empty(); + // Timeout neccessary for IE9 + setTimeout(function () { + document.selection.empty(); + }); } else { window.getSelection().removeAllRanges(); } @@ -418,8 +472,11 @@ _dragStarted: function () { if (rootEl && dragEl) { + var options = this.options; + // Apply effect - _toggleClass(dragEl, this.options.ghostClass, true); + _toggleClass(dragEl, options.ghostClass, true); + _toggleClass(dragEl, options.dragClass, false); Sortable.active = this; @@ -443,12 +500,11 @@ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), parent = target, - groupName = ' ' + this.options.group.name + '', i = touchDragOverListeners.length; if (parent) { do { - if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { + if (parent[expando]) { while (i--) { touchDragOverListeners[i]({ clientX: touchEvt.clientX, @@ -478,9 +534,10 @@ if (tapEvt) { var options = this.options, fallbackTolerance = options.fallbackTolerance, + fallbackOffset = options.fallbackOffset, touch = evt.touches ? evt.touches[0] : evt, - dx = touch.clientX - tapEvt.clientX, - dy = touch.clientY - tapEvt.clientY, + dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x, + dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y, translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; // only set the status to dragging, when we are actually dragging @@ -520,6 +577,7 @@ _toggleClass(ghostEl, options.ghostClass, false); _toggleClass(ghostEl, options.fallbackClass, true); + _toggleClass(ghostEl, options.dragClass, true); _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); @@ -545,13 +603,15 @@ this._offUpEvents(); - if (activeGroup.pull == 'clone') { - cloneEl = dragEl.cloneNode(true); + if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') { + cloneEl = _clone(dragEl); _css(cloneEl, 'display', 'none'); rootEl.insertBefore(cloneEl, dragEl); _dispatchEvent(this, rootEl, 'clone', dragEl); } + _toggleClass(dragEl, options.dragClass, true); + if (useFallback) { if (useFallback === 'touch') { // Bind touch events @@ -581,10 +641,11 @@ var el = this.el, target, dragRect, + targetRect, revert, options = this.options, group = options.group, - groupPut = group.put, + activeSortable = Sortable.active, isOwner = (activeGroup === group), canSort = options.sort; @@ -598,9 +659,9 @@ if (activeGroup && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list - : activeGroup.pull && groupPut && ( - (activeGroup.name === group.name) || // by Name - (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array + : ( + putSortable === this || + activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt) ) ) && (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback @@ -614,6 +675,7 @@ target = _closest(evt.target, options.draggable, el); dragRect = dragEl.getBoundingClientRect(); + putSortable = this; if (revert) { _cloneHide(true); @@ -633,7 +695,6 @@ if ((el.children.length === 0) || (el.children[0] === ghostEl) || (el === evt.target) && (target = _ghostIsLast(el, evt)) ) { - if (target) { if (target.animated) { return; @@ -644,7 +705,7 @@ _cloneHide(isOwner); - if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) { + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) { if (!dragEl.contains(el)) { el.appendChild(dragEl); parentEl = el; // actualization @@ -661,9 +722,9 @@ lastParentCSS = _css(target.parentNode); } + targetRect = target.getBoundingClientRect(); - var targetRect = target.getBoundingClientRect(), - width = targetRect.right - targetRect.left, + var width = targetRect.right - targetRect.left, height = targetRect.bottom - targetRect.top, floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display) || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), @@ -671,7 +732,7 @@ isLong = (target.offsetHeight > dragEl.offsetHeight), halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, nextSibling = target.nextElementSibling, - moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect), + moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt), after ; @@ -784,6 +845,7 @@ } _disableDraggable(dragEl); + dragEl.style['will-change'] = ''; // Remove class's _toggleClass(dragEl, this.options.ghostClass, false); @@ -793,15 +855,16 @@ newIndex = _index(dragEl, options.draggable); if (newIndex >= 0) { - // drag from one list and drop into another - _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex); - _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); // Add event _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex); // Remove event _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex); + + // drag from one list and drop into another + _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex); + _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); } } else { @@ -821,7 +884,8 @@ } if (Sortable.active) { - if (newIndex === null || newIndex === -1) { + /* jshint eqnull:true */ + if (newIndex == null || newIndex === -1) { newIndex = oldIndex; } @@ -837,7 +901,7 @@ this._nulling(); }, - _nulling: function () { + _nulling: function() { rootEl = dragEl = parentEl = @@ -857,6 +921,7 @@ lastEl = lastCSS = + putSortable = activeGroup = Sortable.active = null; }, @@ -1011,14 +1076,21 @@ if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { return el; } - } - while (el !== ctx && (el = el.parentNode)); + /* jshint boss:true */ + } while (el = _getParentOrHost(el)); } return null; } + function _getParentOrHost(el) { + var parent = el.host; + + return (parent && parent.nodeType) ? parent : el.parentNode; + } + + function _globalDragOver(/**Event*/evt) { if (evt.dataTransfer) { evt.dataTransfer.dropEffect = 'move'; @@ -1094,8 +1166,10 @@ function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) { + sortable = (sortable || rootEl[expando]); + var evt = document.createEvent('Event'), - options = (sortable || rootEl[expando]).options, + options = sortable.options, onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); evt.initEvent(name, true, true); @@ -1116,7 +1190,7 @@ } - function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) { + function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) { var evt, sortable = fromEl[expando], onMoveFn = sortable.options.onMove, @@ -1135,7 +1209,7 @@ fromEl.dispatchEvent(evt); if (onMoveFn) { - retVal = onMoveFn.call(sortable, evt); + retVal = onMoveFn.call(sortable, evt, originalEvt); } return retVal; @@ -1155,9 +1229,14 @@ /** @returns {HTMLElement|false} */ function _ghostIsLast(el, evt) { var lastEl = el.lastElementChild, - rect = lastEl.getBoundingClientRect(); - - return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta + rect = lastEl.getBoundingClientRect(); + + // 5 — min delta + // abs — нельзя добавлять, а то глюки при наведении сверху + return ( + (evt.clientY - (rect.top + rect.height) > 5) || + (evt.clientX - (rect.right + rect.width) > 5) + ) && lastEl; } @@ -1251,6 +1330,15 @@ return dst; } + function _clone(el) { + return $ + ? $(el).clone(true)[0] + : (Polymer && Polymer.dom + ? Polymer.dom(el).cloneNode(true) + : el.cloneNode(true) + ); + } + // Export utils Sortable.utils = { @@ -1265,6 +1353,7 @@ throttle: _throttle, closest: _closest, toggleClass: _toggleClass, + clone: _clone, index: _index }; diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index e5df7b9150e..935ceef0680 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -39,3 +39,6 @@ captures/ # Keystore files *.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore index 7a065c709c7..8a365b3d829 100644 --- a/vendor/gitignore/C.gitignore +++ b/vendor/gitignore/C.gitignore @@ -7,6 +7,11 @@ *.obj *.elf +# Linker output +*.ilk +*.map +*.exp + # Precompiled Headers *.gch *.pch @@ -34,3 +39,13 @@ # Debug files *.dSYM/ *.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore index 5ffc21546ec..c92aea0fe0c 100644 --- a/vendor/gitignore/ExtJs.gitignore +++ b/vendor/gitignore/ExtJs.gitignore @@ -1,4 +1,12 @@ .architect +bootstrap.css +bootstrap.js bootstrap.json +bootstrap.jsonp build/ +classic.json +classic.jsonp ext/ +modern.json +modern.jsonp +resources/sass/.sass-cache/ diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index ea83a5eb620..0a254147875 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -4,9 +4,6 @@ # User-specific stuff: .idea/workspace.xml .idea/tasks.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml # Sensitive or high-churn files: .idea/dataSources.ids diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore index 828a509a137..f0f3fbc06c8 100644 --- a/vendor/gitignore/Global/macOS.gitignore +++ b/vendor/gitignore/Global/macOS.gitignore @@ -1,26 +1,26 @@ -*.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk +*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE index 0e259d42c99..670154e3538 100644 --- a/vendor/gitignore/LICENSE +++ b/vendor/gitignore/LICENSE @@ -1,121 +1,116 @@ -Creative Commons Legal Code - CC0 1.0 Universal - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - Statement of Purpose The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +<http://creativecommons.org/publicdomain/zero/1.0/> diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore index 1cd717b6921..e7c594fa3e2 100644 --- a/vendor/gitignore/Laravel.gitignore +++ b/vendor/gitignore/Laravel.gitignore @@ -7,6 +7,7 @@ app/storage/ # Laravel 5 & Lumen specific bootstrap/cache/ +public/storage .env.*.php .env.php .env diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore index abc21828a3e..3f36ea2a878 100644 --- a/vendor/gitignore/Nanoc.gitignore +++ b/vendor/gitignore/Nanoc.gitignore @@ -1,6 +1,6 @@ -# For projects using nanoc (http://nanoc.ws/) +# For projects using Nanoc (http://nanoc.ws/) -# Default location for output, needs to match output_dir's value found in config.yaml +# Default location for output (needs to match output_dir's value found in nanoc.yaml) output/ # Temporary file directory diff --git a/vendor/gitignore/OpenCart.gitignore b/vendor/gitignore/OpenCart.gitignore index 28e45aa6aac..97be41faa38 100644 --- a/vendor/gitignore/OpenCart.gitignore +++ b/vendor/gitignore/OpenCart.gitignore @@ -11,3 +11,10 @@ system/cache/ system/logs/ system/storage/ + +# vQmod log files +vqmod/logs/* +# vQmod cache files +vqmod/vqcache/* +vqmod/checked.cache +vqmod/mods.cache diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 37fc9d40817..6a2bf47ade9 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -66,7 +66,7 @@ docs/_build/ # PyBuilder target/ -# IPython Notebook +# Jupyter Notebook .ipynb_checkpoints # pyenv diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore index cb14a420640..50281a44270 100644 --- a/vendor/gitignore/Rust.gitignore +++ b/vendor/gitignore/Rust.gitignore @@ -5,3 +5,6 @@ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index f620fad23eb..1afbaf197f4 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -61,6 +61,15 @@ acs-*.bib # fixme *.lox +# feynmf/feynmp +*.mf +*.mp +*.t[1-9] +*.t[1-9][0-9] +*.tfm +*.[1-9] +*.[1-9][0-9] + #(r)(e)ledmac/(r)(e)ledpar *.end *.?end diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore index be0e4913c3a..beec7b91f15 100644 --- a/vendor/gitignore/UnrealEngine.gitignore +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -1,6 +1,9 @@ # Visual Studio 2015 user specific files .vs/ +# Visual Studio 2015 database file +*.VC.db + # Compiled Object files *.slo *.lo diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 1b86e7ec918..09e407344ca 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -1,11 +1,14 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates +*.vcxproj.filters # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -44,6 +47,7 @@ dlldata.c project.lock.json project.fragment.lock.json artifacts/ +Properties/launchSettings.json *_i.c *_p.c @@ -238,6 +242,9 @@ FakesAssemblies/ # Visual Studio 6 workspace options file *.opt +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts diff --git a/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml b/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml new file mode 100644 index 00000000000..f066285b1ad --- /dev/null +++ b/vendor/gitlab-ci-yml/Clojure.gitlab-ci.yml @@ -0,0 +1,22 @@ +# Based on openjdk:8, already includes lein +image: clojure:lein-2.7.0 +# If you need to configure a database, add a `services` section here +# See https://docs.gitlab.com/ce/ci/services/postgres.html +# Make sure you configure the connection as well + +before_script: + # If you need to install any external applications, like a + # postgres client, you may want to uncomment the line below: + # + #- apt-get update -y + # + # Retrieve project dependencies + # Do this on before_script since it'll be shared between both test and + # any production sections a user adds + - lein deps + +test: + script: + # If you need to run any migrations or configure the database, this + # would be the point to do it. + - lein test diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml new file mode 100644 index 00000000000..e8da49a935e --- /dev/null +++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml @@ -0,0 +1,37 @@ +# This file is a template, and might need editing before it works on your project. +# Official language image. Look for the different tagged releases at: +# https://hub.docker.com/r/crystallang/crystal/ +image: "crystallang/crystal:latest" + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service +# services: +# - mysql:latest +# - redis:latest +# - postgres:latest + +# variables: +# POSTGRES_DB: database_name + +# Cache shards in between builds +cache: + paths: + - libs + +# This is a basic example for a shard or script which doesn't use +# services such as redis or postgres +before_script: + - apt-get update -qq && apt-get install -y -qq libxml2-dev + - crystal -v # Print out Crystal version for debugging + - shards + +# If you are using built-in Crystal Spec. +spec: + script: + - crystal spec + +# If you are using minitest.cr +minitest: + script: + - crystal test/spec_test.cr # change to the file(s) you execute for tests |