diff options
author | Achilleas Pipinellis <axilleas@axilleas.me> | 2016-10-16 18:16:50 +0000 |
---|---|---|
committer | Achilleas Pipinellis <axilleas@axilleas.me> | 2016-10-16 18:16:50 +0000 |
commit | a4fa44ce90565c93b65b730d9d400e4b4b49ee4d (patch) | |
tree | 1e4037ac6fbb543a207826a61383590b095e1449 | |
parent | 5174b7ad7e4b90761467388a29fa016d77e7a16d (diff) | |
parent | 195c10574e110a0ced395fbee5cd5e546fb9e7fc (diff) | |
download | gitlab-ce-a4fa44ce90565c93b65b730d9d400e4b4b49ee4d.tar.gz |
Merge branch 'master' into 'docs/change-docs-reviewers'
# Conflicts:
# doc/development/doc_styleguide.md
225 files changed, 3428 insertions, 1311 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3b864f16cf..ace0de5cad3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -99,7 +99,7 @@ update-knapsack: - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} - - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' + - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d paths: diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index d2a1eb56423..9b541aadad1 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -1,4 +1,4 @@ -See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html. +See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html ## What does this MR do? diff --git a/.scss-lint.yml b/.scss-lint.yml index 71df6be6a15..5093702519b 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -61,7 +61,7 @@ linters: # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: - enabled: false + enabled: true # Reports when you have an empty rule set. EmptyRule: @@ -219,7 +219,7 @@ linters: # Property values, @extend, @include, and @import directives, and variable # declarations should always end with a semicolon. TrailingSemicolon: - enabled: false + enabled: true # Reports lines containing trailing whitespace. TrailingWhitespace: diff --git a/CHANGELOG b/CHANGELOG index 99bbd99726d..e3201cd2250 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,8 @@ v 8.13.0 (unreleased) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Respond with 404 Not Found for non-existent tags (Linus Thiel) - Truncate long labels with ellipsis in labels page + - Improve tabbing usability for sign in page (ClemMakesApps) + - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint - Adding members no longer silently fails when there is extra whitespace - Update runner version only when updating contacted_at - Add link from system note to compare with previous version @@ -14,6 +16,7 @@ v 8.13.0 (unreleased) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Updating verbiage on git basics to be more intuitive - Clarify documentation for Runners API (Gennady Trafimenkov) + - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username - Prevent running GfmAutocomplete setup for each diff note !6569 - Added documentation for .gitattributes files @@ -24,6 +27,7 @@ v 8.13.0 (unreleased) - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) + - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Add tag shortcut from the Commit page. !6543 - Keep refs for each deployment @@ -50,6 +54,7 @@ v 8.13.0 (unreleased) - Add new issue button to each list on Issues Board - Added soft wrap button to repository file/blob editor - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) + - Show the time ago a merge request was deployed to an environment - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) @@ -112,10 +117,12 @@ v 8.13.0 (unreleased) - Fixes padding in all clipboard icons that have .btn class - Fix a typo in doc/api/labels.md - API: all unknown routing will be handled with 404 Not Found + - Add docs for request profiling - Make guests unable to view MRs on private projects v 8.12.7 - Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659 + - Fix GFM autocomplete setup being called several times v 8.12.6 - Update mailroom to 0.8.1 in Gemfile.lock !6814 @@ -263,6 +270,7 @@ v 8.12.0 - Remove prefixes from transition CSS property (ClemMakesApps) - Add Sentry logging to API calls - Add BroadcastMessage API + - Merge request tabs are fixed when scrolling page - Use 'git update-ref' for safer web commits !6130 - Sort pipelines requested through the API - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling) @@ -369,6 +377,7 @@ v 8.11.7 - Avoid conflict with admin labels when importing GitHub labels. !6158 - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234 - Allow the Rails cookie to be used for API authentication. + - Login/Register UX upgrade !6328 v 8.11.6 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index b60d71966ae..7ada0d303f3 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.8.4 +0.8.5 diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index f336bfc36d6..97462a5959c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -15,18 +15,17 @@ this.hideSidebar = bind(this.hideSidebar, this); this.toggleSidebar = bind(this.toggleSidebar, this); this.updateDropdown = bind(this.updateDropdown, this); + this.$document = $(document); clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); - $('.js-build-sidebar').niceScroll(); + this.initSidebar(); this.populateJobs(this.build_stage); this.updateStageDropdownText(this.build_stage); - this.hideSidebar(); - $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); $(window).off('resize.build').on('resize.build', this.hideSidebar); - $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); $('#js-build-scroll > a').off('click').on('click', this.stepTrace); this.updateArtifactRemoveDate(); if ($('#build-trace').length) { @@ -62,6 +61,21 @@ } } + Build.prototype.initSidebar = function() { + this.$sidebar = $('.js-build-sidebar'); + this.sidebarTranslationLimits = { + min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + } + this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight(); + this.$sidebar.css({ + top: this.sidebarTranslationLimits.max + }); + this.$sidebar.niceScroll(); + this.hideSidebar(); + this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); + this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this)); + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] @@ -129,15 +143,23 @@ Build.prototype.toggleSidebar = function() { if (this.shouldHideSidebar()) { - return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed'); + return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed'); } }; + Build.prototype.translateSidebar = function(e) { + var newPosition = this.sidebarTranslationLimits.max - document.body.scrollTop; + if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; + this.$sidebar.css({ + top: newPosition + }); + }; + Build.prototype.hideSidebar = function() { if (this.shouldHideSidebar()) { - return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); } else { - return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); } }; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js.es6 index f3ef13ce20e..f3957ed374b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -8,6 +8,7 @@ Dispatcher = (function() { function Dispatcher() { this.initSearch(); + this.initFieldErrors(); this.initPageScripts(); } @@ -20,6 +21,9 @@ path = page.split(':'); shortcut_handler = null; switch (page) { + case 'sessions:new': + new UsernameValidator(); + break; case 'projects:boards:show': case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); @@ -140,12 +144,12 @@ break; case 'groups:group_members:index': new gl.MemberExpirationDate(); - new GroupMembers(); + new gl.Members(); new UsersSelect(); break; case 'projects:project_members:index': new gl.MemberExpirationDate(); - new ProjectMembers(); + new gl.Members(); new UsersSelect(); break; case 'groups:new': @@ -291,6 +295,12 @@ } }; + Dispatcher.prototype.initFieldErrors = function() { + $('.show-gl-field-errors').each((i, form) => { + new gl.GlFieldErrors(form); + }); + }; + return Dispatcher; })(); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index e034ca68645..53762f2965c 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -25,7 +25,7 @@ return function(e) { e.preventDefault(); e.stopPropagation(); - return _this.input.val('').trigger('keyup').focus(); + return _this.input.val('').trigger('input').focus(); }; })(this)); // Key events @@ -37,28 +37,16 @@ e.preventDefault() } }) - .on('keyup', function(e) { - var keyCode; - keyCode = e.which; - if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) { - return; - } + .on('input', function() { if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.addClass(HAS_VALUE_CLASS); } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { $inputContainer.removeClass(HAS_VALUE_CLASS); } - if (keyCode === 13 && !options.elIsInput) { - return false; - } // Only filter asynchronously only if option remote is set if (this.options.remote) { clearTimeout(timeout); return timeout = setTimeout(function() { - var blurField = this.shouldBlur(keyCode); - if (blurField && this.filterInputBlur) { - this.input.blur(); - } return this.options.query(this.input.val(), function(data) { return this.options.callback(data); }.bind(this)); @@ -255,7 +243,7 @@ _this.fullData = data; _this.parseData(_this.fullData); if (_this.options.filterable && _this.filter && _this.filter.input) { - return _this.filter.input.trigger('keyup'); + return _this.filter.input.trigger('input'); } }; // Remote data @@ -487,7 +475,7 @@ // Triggering 'keyup' will re-render the dropdown which is not always required // specially if we want to keep the state of the dropdown needed for bulk-assignment if (!this.options.persistWhenHide) { - $input.trigger("keyup"); + $input.trigger("input"); } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); @@ -500,14 +488,27 @@ // Render the full menu GitLabDropdown.prototype.renderMenu = function(html) { - var menu_html; - menu_html = ""; if (this.options.renderMenu) { - menu_html = this.options.renderMenu(html); + return this.options.renderMenu(html); } else { - menu_html = $('<ul />').append(html); + var ul = document.createElement('ul'); + + for (var i = 0; i < html.length; i++) { + var el = html[i]; + + if (el instanceof jQuery) { + el = el.get(0); + } + + if (typeof el === 'string') { + ul.innerHTML += el; + } else { + ul.appendChild(el); + } + } + + return ul; } - return menu_html; }; // Append the menu into the dropdown @@ -521,7 +522,7 @@ }; GitLabDropdown.prototype.renderItem = function(data, group, index) { - var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value; + var field, fieldName, html, selected, text, url, value; if (group == null) { group = false; } @@ -529,18 +530,16 @@ // Render the row index = false; } - html = ""; - // Divider - if (data === "divider") { - return "<li class='divider'></li>"; - } - // Separator is a full-width divider - if (data === "separator") { - return "<li class='separator'></li>"; + html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { + html.className = data; + return html; } // Header if (data.header != null) { - return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header }); + html.className = 'dropdown-header'; + html.innerHTML = data.header; + return html; } if (this.options.renderRow) { // Call the render function @@ -567,24 +566,25 @@ } else { text = data.text != null ? data.text : ''; } - cssClass = ""; - if (selected) { - cssClass = "is-active"; - } if (this.highlight) { text = this.highlightTextMatches(text, this.filterInput.val()); } + // Create the list item & the link + var link = document.createElement('a'); + + link.href = url; + link.innerHTML = text; + + if (selected) { + link.className = 'is-active'; + } + if (group) { - groupAttrs = 'data-group=' + group + ' data-index=' + index; - } else { - groupAttrs = ''; + link.dataset.group = group; + link.dataset.index = index; } - html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({ - url: url, - groupAttrs: groupAttrs, - cssClass: cssClass, - text: text - }); + + html.appendChild(link); } return html; }; diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 new file mode 100644 index 00000000000..8657e7b4abf --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -0,0 +1,167 @@ +((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(); + } + + } + + 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'; + + class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } + + initValidators () { + // select all non-hidden inputs in form + this.state.inputs = this.form.find(':input:not([type=hidden])').toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new GlFieldError({ input, formErrors: this })); + + this.form.on('submit', this.catchInvalidFormSubmit); + } + + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ + + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + } + + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); + } + } + + global.GlFieldErrors = GlFieldErrors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js deleted file mode 100644 index 4382dd6860f..00000000000 --- a/app/assets/javascripts/groups.js +++ /dev/null @@ -1,13 +0,0 @@ -(function() { - this.GroupMembers = (function() { - function GroupMembers() { - $('li.group_member').bind('ajax:success', function() { - return $(this).fadeOut(); - }); - } - - return GroupMembers; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 1935af491f7..e1532fd9ec4 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -14,14 +14,18 @@ inputs.datepicker({ dateFormat: 'yy-mm-dd', minDate: 1, - onSelect: toggleClearInput + onSelect: function () { + $(this).trigger('change'); + toggleClearInput.call(this); + } }); inputs.next('.js-clear-input').on('click', function(event) { event.preventDefault(); var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); - input.datepicker('setDate', null); + input.datepicker('setDate', null) + .trigger('change'); toggleClearInput.call(input); }); diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 new file mode 100644 index 00000000000..a0cd20f21e8 --- /dev/null +++ b/app/assets/javascripts/members.js.es6 @@ -0,0 +1,36 @@ +((w) => { + w.gl = w.gl || {}; + + class Members { + constructor() { + this.addListeners(); + } + + addListeners() { + $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); + $('.js-member-update-control').off('change').on('change', this.formSubmit); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + } + + removeRow(e) { + const $target = $(e.target); + + if ($target.hasClass('btn-remove')) { + $target.closest('.member') + .fadeOut(function () { + $(this).remove(); + }); + } + } + + formSubmit() { + $(this).closest('form').trigger("submit.rails").end().disable(); + } + + formSuccess() { + $(this).find('.js-member-update-control').enable(); + } + } + + gl.Members = Members; +})(window); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 8045d24a1bb..fd21aa1fefa 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -71,6 +71,7 @@ this._location = location; this.bindEvents(); this.activateTab(this.opts.action); + this.initAffix(); } MergeRequestTabs.prototype.bindEvents = function() { @@ -380,6 +381,46 @@ // Only when sidebar is collapsed }; + MergeRequestTabs.prototype.initAffix = function () { + var $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + var tabsWidth = $tabs.outerWidth(), + $diffTabs = $('#diff-notes-app'), + offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height()); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: offsetTop + } + }).on('affix.bs.affix', function () { + $tabs.css({ + left: $tabs.offset().left, + width: tabsWidth + }); + $diffTabs.css({ + marginTop: $tabs.height() + }); + }).on('affix-top.bs.affix', function () { + $tabs.css({ + left: '', + width: '' + }); + $diffTabs.css({ + marginTop: '' + }); + }); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + }; + return MergeRequestTabs; })(); diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js.es6 index 7bbcdf59838..fcadc4bc515 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,7 +1,26 @@ -(function() { + ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - this.MergeRequestWidget = (function() { + const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> + <div class="ci_widget ci-success"> + <%= ci_success_icon %> + <span> + Deployed to + <a href="<%- url %>" target="_blank" class="environment"> + <%- name %> + </a> + <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> + <%- deployed_at %> + </span> + <a class="js-environment-link" href="<%- external_url %>" target="_blank"> + <i class="fa fa-external-link"></i> + View on <%- external_url_formatted %> + </a> + </span> + </div> + </div>`; + + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -10,17 +29,23 @@ // ci_status_url - String, URL to use to check CI status // this.opts = opts; + this.$widgetBody = $('.mr-widget-body'); $('#modal_merge_info').modal({ show: false }); this.firstCICheck = true; this.readyForCICheck = false; + this.readyForCIEnvironmentCheck = false; this.cancel = false; clearInterval(this.fetchBuildStatusInterval); + clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); + this.getCIEnvironmentsStatus(); + this.retrieveSuccessIcon(); this.pollCIStatus(); + this.pollCIEnvironmentsStatus(); notifyPermissions(); } @@ -41,6 +66,7 @@ page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { clearInterval(_this.fetchBuildStatusInterval); + clearInterval(_this.fetchBuildEnvironmentStatusInterval); _this.cancelPolling(); return _this.clearEventListeners(); } @@ -48,6 +74,12 @@ })(this)); }; + MergeRequestWidget.prototype.retrieveSuccessIcon = function() { + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { deleteSourceBranch = false; @@ -62,7 +94,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); @@ -118,6 +150,7 @@ if (data.status === '') { return; } + if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); @@ -150,6 +183,41 @@ })(this)); }; + MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { + this.fetchBuildEnvironmentStatusInterval = setInterval(() => { + if (!this.readyForCIEnvironmentCheck) return; + this.getCIEnvironmentsStatus(); + this.readyForCIEnvironmentCheck = false; + }, 300000); + }; + + MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { + $.getJSON(this.opts.ci_environments_status_url, (environments) => { + if (this.cancel) return; + this.readyForCIEnvironmentCheck = true; + if (environments && environments.length) this.renderEnvironments(environments); + }); + }; + + MergeRequestWidget.prototype.renderEnvironments = function(environments) { + for (let i = 0; i < environments.length; i++) { + const environment = environments[i]; + if ($(`.mr-state-widget #${ environment.id }`).length) return; + const $template = $(DEPLOYMENT_TEMPLATE); + if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); + if (environment.deployed_at && environment.deployed_at_formatted) { + environment.deployed_at = $.timeago(environment.deployed_at) + '.'; + } else { + $('.js-environment-timeago', $template).remove(); + environment.name += '.'; + } + environment.ci_success_icon = this.$ciSuccessIcon; + const templateString = _.unescape($template[0].outerHTML); + const template = _.template(templateString)(environment) + this.$widgetBody.before(template); + } + }; + MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; if (state == null) { @@ -190,4 +258,4 @@ })(); -}).call(this); + })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 6bf63ee6979..6bf63ee6979 100644 --- a/app/assets/javascripts/pipeline.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js deleted file mode 100644 index 78f7b48bc7d..00000000000 --- a/app/assets/javascripts/project_members.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { - this.ProjectMembers = (function() { - function ProjectMembers() { - $('li.project_member').bind('ajax:success', function() { - return $(this).fadeOut(); - }); - } - return ProjectMembers; - })(); -}).call(this); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index 2ecf3b18975..bd4e3c3d00d 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -16,7 +16,13 @@ if (initialQuery.name) this.requestFile(initialQuery); $('.reset-template', this.dropdown.parent()).on('click', () => { - if (this.currentTemplate) this.setInputValueToTemplateContent(false); + this.setInputValueToTemplateContent(); + }); + + $('.no-template', this.dropdown.parent()).on('click', () => { + this.currentTemplate = ''; + this.setInputValueToTemplateContent(); + $('.dropdown-toggle-text', this.dropdown).text('Choose a template'); }); } diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 new file mode 100644 index 00000000000..2517f778365 --- /dev/null +++ b/app/assets/javascripts/username_validator.js.es6 @@ -0,0 +1,133 @@ +((global) => { + const debounceTimeoutDuration = 1000; + const invalidInputClass = 'gl-field-error-outline'; + const successInputClass = 'gl-field-success-outline'; + const unavailableMessageSelector = '.username .validation-error'; + const successMessageSelector = '.username .validation-success'; + const pendingMessageSelector = '.username .validation-pending'; + const invalidMessageSelector = '.username .gl-field-error'; + + class UsernameValidator { + constructor() { + this.inputElement = $('#new_user_username'); + this.inputDomElement = this.inputElement.get(0); + this.state = { + available: false, + valid: false, + pending: false, + empty: true + }; + + const debounceTimeout = _.debounce((username) => { + this.validateUsername(username); + }, debounceTimeoutDuration); + + this.inputElement.on('keyup.username_check', () => { + const username = this.inputElement.val(); + + this.state.valid = this.inputDomElement.validity.valid; + this.state.empty = !username.length; + + if (this.state.valid) { + return debounceTimeout(username); + } + + this.renderState(); + }); + + // Override generic field validation + this.inputElement.on('invalid', this.interceptInvalid.bind(this)); + } + + renderState() { + // Clear all state + this.clearFieldValidationState(); + + if (this.state.valid && this.state.available) { + return this.setSuccessState(); + } + + if (this.state.empty) { + return this.clearFieldValidationState(); + } + + if (this.state.pending) { + return this.setPendingState(); + } + + if (!this.state.available) { + return this.setUnavailableState(); + } + + if (!this.state.valid) { + return this.setInvalidState(); + } + } + + interceptInvalid(event) { + event.preventDefault(); + event.stopPropagation(); + } + + validateUsername(username) { + if (this.state.valid) { + this.state.pending = true; + this.state.available = false; + this.renderState(); + return $.ajax({ + type: 'GET', + url: `/u/${username}/exists`, + dataType: 'json', + success: (res) => this.setAvailabilityState(res.exists) + }); + } + } + + setAvailabilityState(usernameTaken) { + if (usernameTaken) { + this.state.valid = false; + this.state.available = false; + } else { + this.state.available = true; + } + this.state.pending = false; + this.renderState(); + } + + clearFieldValidationState() { + this.inputElement.siblings('p').hide(); + + this.inputElement.removeClass(invalidInputClass) + .removeClass(successInputClass); + } + + setUnavailableState() { + const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $usernameUnavailableMessage.show(); + } + + setSuccessState() { + const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); + this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); + $usernameSuccessMessage.show(); + } + + setPendingState() { + const $usernamePendingMessage = $(pendingMessageSelector); + if (this.state.pending) { + $usernamePendingMessage.show(); + } else { + $usernamePendingMessage.hide(); + } + } + + setInvalidState() { + const $inputErrorMessage = $(invalidMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $inputErrorMessage.show(); + } + } + + global.UsernameValidator = UsernameValidator; +})(window); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 6aa0e1cd2b6..3020b7cc239 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -325,6 +325,10 @@ }; UsersSelect.prototype.user = function(user_id, callback) { + if(!/^\d+$/.test(user_id)) { + return false; + } + var url; url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 897bc49e7df..e3ca7f6373a 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -5,6 +5,7 @@ display: none; &.hide { display: block; } } + &.open .content { display: block; &.hide { display: none; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 8002e56724b..df2e2ea8d2c 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -19,6 +19,7 @@ &.diff-collapsed { padding: 5px; + .click-to-expand { cursor: pointer; } @@ -203,6 +204,7 @@ } } } + &.user-cover-block { padding: 24px 0 0; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a7c8d782e9b..e6656c2d69a 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -25,7 +25,7 @@ &:focus { background-color: $hover-background; color: $hover-text; - border-color: $hover-border;; + border-color: $hover-border; } } @@ -152,7 +152,8 @@ @include btn-blue-medium; } - &.btn-info { + &.btn-info, + &.btn-register { @include btn-blue; } @@ -240,6 +241,7 @@ width: 100%; margin: 0; margin-bottom: 15px; + &.btn { padding: 6px 0; } @@ -321,6 +323,7 @@ .btn-build { margin-left: 10px; + i { color: $gl-icon-color; } @@ -328,6 +331,7 @@ .clone-dropdown-btn a { color: $dropdown-link-color; + &:hover { text-decoration: none; } @@ -337,6 +341,7 @@ background-color: $background-color !important; border: 1px solid lightgrey; cursor: default; + &:active { -moz-box-shadow: inset 0 0 0 white; -webkit-box-shadow: inset 0 0 0 white; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index da7bab74a32..f3b6ad88ad6 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -13,10 +13,12 @@ color: $text-color; background: $background-color; } + .bs-callout h4 { margin-top: 0; margin-bottom: 5px; } + .bs-callout p:last-child { margin-bottom: 0; } @@ -27,16 +29,19 @@ border-color: #eed3d7; color: #b94a48; } + .bs-callout-warning { background-color: #faf8f0; border-color: #faebcc; color: #8a6d3b; } + .bs-callout-info { background-color: #f4f8fa; border-color: #bce8f1; color: #34789a; } + .bs-callout-success { background-color: #dff0d8; border-color: #5ca64d; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5957dce89bc..81e4e264560 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,31 +1,31 @@ /** COLORS **/ .cgray { color: $gl-gray; } -.clgray { color: #bbb } +.clgray { color: #bbb; } .cred { color: $gl-text-red; } .cgreen { color: $gl-text-green; } -.cdark { color: #444 } +.cdark { color: #444; } /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-5 { margin-top: 5px; } -.prepend-top-10 { margin-top: 10px } +.prepend-top-10 { margin-top: 10px; } .prepend-top-default { margin-top: $gl-padding !important; } -.prepend-top-20 { margin-top: 20px } -.prepend-left-5 { margin-left: 5px } -.prepend-left-10 { margin-left: 10px } +.prepend-top-20 { margin-top: 20px; } +.prepend-left-5 { margin-left: 5px; } +.prepend-left-10 { margin-left: 10px; } .prepend-left-default { margin-left: $gl-padding; } -.prepend-left-20 { margin-left: 20px } -.append-right-5 { margin-right: 5px } -.append-right-10 { margin-right: 10px } +.prepend-left-20 { margin-left: 20px; } +.append-right-5 { margin-right: 5px; } +.append-right-10 { margin-right: 10px; } .append-right-default { margin-right: $gl-padding; } -.append-right-20 { margin-right: 20px } -.append-bottom-0 { margin-bottom: 0 } -.append-bottom-10 { margin-bottom: 10px } -.append-bottom-15 { margin-bottom: 15px } -.append-bottom-20 { margin-bottom: 20px } +.append-right-20 { margin-right: 20px; } +.append-bottom-0 { margin-bottom: 0; } +.append-bottom-10 { margin-bottom: 10px; } +.append-bottom-15 { margin-bottom: 15px; } +.append-bottom-20 { margin-bottom: 20px; } .append-bottom-default { margin-bottom: $gl-padding; } -.inline { display: inline-block } -.center { text-align: center } +.inline { display: inline-block; } +.center { text-align: center; } .underlined-link { text-decoration: underline; } .hint { font-style: italic; color: #999; } @@ -97,6 +97,7 @@ span.update-author { color: #999; font-weight: normal; font-style: italic; + strong { font-weight: bold; font-style: normal; @@ -128,7 +129,7 @@ p.time { // Fix issue with notes & lists creating a bunch of bottom borders. li.note { - img { max-width: 100% } + img { max-width: 100%; } .note-title { li { border-bottom: none !important; @@ -172,6 +173,7 @@ li.note { @extend .col-md-6; text-align: left; margin-top: 40px; + pre { background: white; border: none; @@ -197,6 +199,7 @@ li.note { background: #c67; color: #fff; font-weight: bold; + a { color: #fff; text-decoration: underline; @@ -227,6 +230,7 @@ li.note { &.milestone-closed { background: $gray-light; } + .progress { margin-bottom: 0; margin-top: 4px; @@ -286,6 +290,7 @@ table { .footer-links { margin-bottom: 20px; + a { margin-right: 15px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index baa95711329..a839371a6f2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -12,6 +12,7 @@ .dropdown-menu, .dropdown-menu-nav { display: block; + @media (max-width: $screen-xs-max) { width: 100%; } @@ -48,6 +49,7 @@ margin-top: -6px; color: $dropdown-toggle-icon-color; font-size: 10px; + &.fa-spinner { font-size: 16px; margin-top: -8px; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 81520500594..13c1bbf0359 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -26,15 +26,6 @@ padding: 10px $gl-padding; word-wrap: break-word; border-radius: 3px 3px 0 0; - cursor: pointer; - - &:hover { - background-color: $dark-background-color; - } - - .diff-toggle-caret { - padding-right: 6px; - } &.file-title-clear { padding-left: 0; @@ -66,6 +57,7 @@ margin-top: -3px; } } + .file-content { background: #fff; @@ -105,22 +97,27 @@ border: none; margin: 0; } + tr { border-bottom: 1px solid #eee; } + td { &:first-child { border-left: none; } + &:last-child { border-right: none; } } + td.blame-commit { padding: 0 10px; min-width: 400px; background: $gray-light; } + td.line-numbers { float: none; border-left: 1px solid #ddd; @@ -130,6 +127,7 @@ margin-right: 0; } } + td.lines { padding: 0; } @@ -146,8 +144,10 @@ border-left: 1px solid $border-color; margin-bottom: 0; background: white; + li { color: #888; + p { margin: 0; color: #333; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 05e8ee0190d..761c07384f4 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -9,7 +9,7 @@ input { input[type='text'].danger { background: #f2dede!important; border-color: #d66; - text-shadow: 0 1px 1px #fff + text-shadow: 0 1px 1px #fff; } .datetime-controls { @@ -73,8 +73,8 @@ label { } .form-control { - box-shadow: none; - border-radius: 3px; + @include box-shadow(none); + border-radius: 2px; padding: $gl-vert-padding $gl-input-padding; } @@ -117,9 +117,11 @@ label { display: table-cell; width: 200px !important; } + .input-group-addon { background-color: #f7f8fa; } + .input-group-addon:not(:first-child):not(:last-child) { border-left: 0; border-right: 0; @@ -129,3 +131,8 @@ label { .help-block { margin-bottom: 0; } + +.gl-field-error { + color: $red-normal; +} + diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 3673b81f183..fe834f4e2f6 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -62,7 +62,7 @@ } i { - color: $white-light + color: $white-light; } path, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 9823abdde1f..3a4fdd0da22 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -168,6 +168,7 @@ header { a { color: $gl-text-color; + &:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index efc348214c2..4b2627c1b87 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -60,6 +60,7 @@ padding-top: 1px; margin: 0; color: $gray-dark; + img { position: relative; top: 3px; @@ -128,6 +129,10 @@ ul.content-list { color: $gl-dark-link-color; } + .member-group-link { + color: $blue-normal; + } + .description { p { @include str-truncated; @@ -168,6 +173,14 @@ ul.content-list { } } + .member-controls { + float: none; + + @media (min-width: $screen-sm-min) { + float: right; + } + } + // When dragging a list item &.ui-sortable-helper { border-bottom: none; diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss index c214eabcad7..a90e45bb5f4 100644 --- a/app/assets/stylesheets/framework/logo.scss +++ b/app/assets/stylesheets/framework/logo.scss @@ -37,6 +37,7 @@ 0%, 10%, 100% { fill: lighten($tanuki-yellow, 25%); } + 90% { fill: $tanuki-yellow; } @@ -48,6 +49,7 @@ 10%, 80% { fill: $tanuki-orange; } + 20%, 90% { fill: lighten($tanuki-orange, 25%); } @@ -59,6 +61,7 @@ 10%, 80% { fill: $tanuki-red; } + 20%, 90% { fill: lighten($tanuki-red, 25%); } @@ -70,6 +73,7 @@ 20%, 70% { fill: $tanuki-red; } + 30%, 80% { fill: lighten($tanuki-red, 25%); } @@ -81,6 +85,7 @@ 30%, 60% { fill: $tanuki-orange; } + 40%, 70% { fill: lighten($tanuki-orange, 25%); } @@ -92,6 +97,7 @@ 30%, 60% { fill: $tanuki-red; } + 40%, 70% { fill: lighten($tanuki-red, 25%); } @@ -103,6 +109,7 @@ 40% { fill: $tanuki-yellow; } + 60% { fill: lighten($tanuki-yellow, 25%); } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 7fabf27a558..f84ca36d10f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -34,6 +34,7 @@ &.active { background: $gray-light; + a { font-weight: 600; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ea43f4afc37..899db045b74 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -210,6 +210,7 @@ @media (max-width: $screen-xs-max) { padding-bottom: 0; width: 100%; + .btn, form, .dropdown, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; display: block; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index c6f30e144fd..5ba0486177f 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -13,6 +13,11 @@ .dropdown-menu-toggle { line-height: 20px; } + + .badge { + margin-top: -2px; + margin-left: 5px; + } } .panel-body { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 79cd26714a3..e0708c65695 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -93,7 +93,7 @@ background: none; .select2-search-field input { - padding: $gl-padding / 2; + padding: 5px $gl-padding / 2; font-size: 13px; height: auto; font-family: inherit; @@ -101,7 +101,7 @@ } .select2-search-choice { - margin: 8px 0 0 8px; + margin: 5px 0 0 8px; box-shadow: none; border-color: $input-border; color: $gl-text-color; @@ -137,6 +137,7 @@ .select2-results { max-height: 350px; + .select2-highlighted { background: $gl-primary; } @@ -212,9 +213,11 @@ .group-image { float: left; } + .group-name { font-weight: bold; } + .group-path { color: #999; } @@ -239,6 +242,7 @@ color: #aaa; font-weight: normal; } + .namespace-path { margin-left: 10px; font-weight: bolder; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 0b0bd80c326..eb63a9f214b 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -48,6 +48,7 @@ &:before { background: none; } + .timeline-entry .timeline-entry-inner { .timeline-icon { display: none; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index e3154657c54..f4106641269 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -48,31 +48,40 @@ .clearfix { @include clearfix(); } + .center-block { @include center-block(); } + .pull-right { float: right !important; } + .pull-left { float: left !important; } + .hide { display: none; } + .show { display: block !important; } + .invisible { visibility: hidden; } + .text-hide { @include text-hide(); } + .hidden { display: none !important; visibility: hidden !important; } + .affix { position: fixed; } @@ -146,6 +155,7 @@ padding: 6px 15px; font-size: 13px; font-weight: normal; + a { color: #777; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d099a884f54..8df0067fac1 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -106,6 +106,7 @@ @extend .table-bordered; margin: 12px 0; color: #5c5d5e; + th { background: #f8fafc; } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 16ffbe57a99..a3acee299e3 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -55,68 +55,68 @@ color: #000 !important; } - .hll { background-color: #373b41 } - .c { color: #969896 } /* Comment */ - .err { color: #c66 } /* Error */ - .k { color: #b294bb } /* Keyword */ - .l { color: #de935f } /* Literal */ - .n { color: #c5c8c6 } /* Name */ - .o { color: #8abeb7 } /* Operator */ - .p { color: #c5c8c6 } /* Punctuation */ - .cm { color: #969896 } /* Comment.Multiline */ - .cp { color: #969896 } /* Comment.Preproc */ - .c1 { color: #969896 } /* Comment.Single */ - .cs { color: #969896 } /* Comment.Special */ - .gd { color: #c66 } /* Generic.Deleted */ - .ge { font-style: italic } /* Generic.Emph */ - .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */ - .gi { color: #b5bd68 } /* Generic.Inserted */ - .gp { color: #969896; font-weight: bold } /* Generic.Prompt */ - .gs { font-weight: bold } /* Generic.Strong */ - .gu { color: #8abeb7; font-weight: bold } /* Generic.Subheading */ - .kc { color: #b294bb } /* Keyword.Constant */ - .kd { color: #b294bb } /* Keyword.Declaration */ - .kn { color: #8abeb7 } /* Keyword.Namespace */ - .kp { color: #b294bb } /* Keyword.Pseudo */ - .kr { color: #b294bb } /* Keyword.Reserved */ - .kt { color: #f0c674 } /* Keyword.Type */ - .ld { color: #b5bd68 } /* Literal.Date */ - .m { color: #de935f } /* Literal.Number */ - .s { color: #b5bd68 } /* Literal.String */ - .na { color: #81a2be } /* Name.Attribute */ - .nb { color: #c5c8c6 } /* Name.Builtin */ - .nc { color: #f0c674 } /* Name.Class */ - .no { color: #c66 } /* Name.Constant */ - .nd { color: #8abeb7 } /* Name.Decorator */ - .ni { color: #c5c8c6 } /* Name.Entity */ - .ne { color: #c66 } /* Name.Exception */ - .nf { color: #81a2be } /* Name.Function */ - .nl { color: #c5c8c6 } /* Name.Label */ - .nn { color: #f0c674 } /* Name.Namespace */ - .nx { color: #81a2be } /* Name.Other */ - .py { color: #c5c8c6 } /* Name.Property */ - .nt { color: #8abeb7 } /* Name.Tag */ - .nv { color: #c66 } /* Name.Variable */ - .ow { color: #8abeb7 } /* Operator.Word */ - .w { color: #c5c8c6 } /* Text.Whitespace */ - .mf { color: #de935f } /* Literal.Number.Float */ - .mh { color: #de935f } /* Literal.Number.Hex */ - .mi { color: #de935f } /* Literal.Number.Integer */ - .mo { color: #de935f } /* Literal.Number.Oct */ - .sb { color: #b5bd68 } /* Literal.String.Backtick */ - .sc { color: #c5c8c6 } /* Literal.String.Char */ - .sd { color: #969896 } /* Literal.String.Doc */ - .s2 { color: #b5bd68 } /* Literal.String.Double */ - .se { color: #de935f } /* Literal.String.Escape */ - .sh { color: #b5bd68 } /* Literal.String.Heredoc */ - .si { color: #de935f } /* Literal.String.Interpol */ - .sx { color: #b5bd68 } /* Literal.String.Other */ - .sr { color: #b5bd68 } /* Literal.String.Regex */ - .s1 { color: #b5bd68 } /* Literal.String.Single */ - .ss { color: #b5bd68 } /* Literal.String.Symbol */ - .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */ - .vc { color: #c66 } /* Name.Variable.Class */ - .vg { color: #c66 } /* Name.Variable.Global */ - .vi { color: #c66 } /* Name.Variable.Instance */ - .il { color: #de935f } /* Literal.Number.Integer.Long */ + .hll { background-color: #373b41; } + .c { color: #969896; } /* Comment */ + .err { color: #c66; } /* Error */ + .k { color: #b294bb; } /* Keyword */ + .l { color: #de935f; } /* Literal */ + .n { color: #c5c8c6; } /* Name */ + .o { color: #8abeb7; } /* Operator */ + .p { color: #c5c8c6; } /* Punctuation */ + .cm { color: #969896; } /* Comment.Multiline */ + .cp { color: #969896; } /* Comment.Preproc */ + .c1 { color: #969896; } /* Comment.Single */ + .cs { color: #969896; } /* Comment.Special */ + .gd { color: #c66; } /* Generic.Deleted */ + .ge { font-style: italic; } /* Generic.Emph */ + .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */ + .gi { color: #b5bd68; } /* Generic.Inserted */ + .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */ + .gs { font-weight: bold; } /* Generic.Strong */ + .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */ + .kc { color: #b294bb; } /* Keyword.Constant */ + .kd { color: #b294bb; } /* Keyword.Declaration */ + .kn { color: #8abeb7; } /* Keyword.Namespace */ + .kp { color: #b294bb; } /* Keyword.Pseudo */ + .kr { color: #b294bb; } /* Keyword.Reserved */ + .kt { color: #f0c674; } /* Keyword.Type */ + .ld { color: #b5bd68; } /* Literal.Date */ + .m { color: #de935f; } /* Literal.Number */ + .s { color: #b5bd68; } /* Literal.String */ + .na { color: #81a2be; } /* Name.Attribute */ + .nb { color: #c5c8c6; } /* Name.Builtin */ + .nc { color: #f0c674; } /* Name.Class */ + .no { color: #c66; } /* Name.Constant */ + .nd { color: #8abeb7; } /* Name.Decorator */ + .ni { color: #c5c8c6; } /* Name.Entity */ + .ne { color: #c66; } /* Name.Exception */ + .nf { color: #81a2be; } /* Name.Function */ + .nl { color: #c5c8c6; } /* Name.Label */ + .nn { color: #f0c674; } /* Name.Namespace */ + .nx { color: #81a2be; } /* Name.Other */ + .py { color: #c5c8c6; } /* Name.Property */ + .nt { color: #8abeb7; } /* Name.Tag */ + .nv { color: #c66; } /* Name.Variable */ + .ow { color: #8abeb7; } /* Operator.Word */ + .w { color: #c5c8c6; } /* Text.Whitespace */ + .mf { color: #de935f; } /* Literal.Number.Float */ + .mh { color: #de935f; } /* Literal.Number.Hex */ + .mi { color: #de935f; } /* Literal.Number.Integer */ + .mo { color: #de935f; } /* Literal.Number.Oct */ + .sb { color: #b5bd68; } /* Literal.String.Backtick */ + .sc { color: #c5c8c6; } /* Literal.String.Char */ + .sd { color: #969896; } /* Literal.String.Doc */ + .s2 { color: #b5bd68; } /* Literal.String.Double */ + .se { color: #de935f; } /* Literal.String.Escape */ + .sh { color: #b5bd68; } /* Literal.String.Heredoc */ + .si { color: #de935f; } /* Literal.String.Interpol */ + .sx { color: #b5bd68; } /* Literal.String.Other */ + .sr { color: #b5bd68; } /* Literal.String.Regex */ + .s1 { color: #b5bd68; } /* Literal.String.Single */ + .ss { color: #b5bd68; } /* Literal.String.Symbol */ + .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */ + .vc { color: #c66; } /* Name.Variable.Class */ + .vg { color: #c66; } /* Name.Variable.Global */ + .vi { color: #c66; } /* Name.Variable.Instance */ + .il { color: #de935f; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 7de920e074b..e9228c94db9 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -55,65 +55,65 @@ color: #000 !important; } - .hll { background-color: #49483e } - .c { color: #75715e } /* Comment */ - .err { color: #960050; background-color: #1e0010 } /* Error */ - .k { color: #66d9ef } /* Keyword */ - .l { color: #ae81ff } /* Literal */ - .n { color: #f8f8f2 } /* Name */ - .o { color: #f92672 } /* Operator */ - .p { color: #f8f8f2 } /* Punctuation */ - .cm { color: #75715e } /* Comment.Multiline */ - .cp { color: #75715e } /* Comment.Preproc */ - .c1 { color: #75715e } /* Comment.Single */ - .cs { color: #75715e } /* Comment.Special */ - .ge { font-style: italic } /* Generic.Emph */ - .gs { font-weight: bold } /* Generic.Strong */ - .kc { color: #66d9ef } /* Keyword.Constant */ - .kd { color: #66d9ef } /* Keyword.Declaration */ - .kn { color: #f92672 } /* Keyword.Namespace */ - .kp { color: #66d9ef } /* Keyword.Pseudo */ - .kr { color: #66d9ef } /* Keyword.Reserved */ - .kt { color: #66d9ef } /* Keyword.Type */ - .ld { color: #e6db74 } /* Literal.Date */ - .m { color: #ae81ff } /* Literal.Number */ - .s { color: #e6db74 } /* Literal.String */ - .na { color: #a6e22e } /* Name.Attribute */ - .nb { color: #f8f8f2 } /* Name.Builtin */ - .nc { color: #a6e22e } /* Name.Class */ - .no { color: #66d9ef } /* Name.Constant */ - .nd { color: #a6e22e } /* Name.Decorator */ - .ni { color: #f8f8f2 } /* Name.Entity */ - .ne { color: #a6e22e } /* Name.Exception */ - .nf { color: #a6e22e } /* Name.Function */ - .nl { color: #f8f8f2 } /* Name.Label */ - .nn { color: #f8f8f2 } /* Name.Namespace */ - .nx { color: #a6e22e } /* Name.Other */ - .py { color: #f8f8f2 } /* Name.Property */ - .nt { color: #f92672 } /* Name.Tag */ - .nv { color: #f8f8f2 } /* Name.Variable */ - .ow { color: #f92672 } /* Operator.Word */ - .w { color: #f8f8f2 } /* Text.Whitespace */ - .mf { color: #ae81ff } /* Literal.Number.Float */ - .mh { color: #ae81ff } /* Literal.Number.Hex */ - .mi { color: #ae81ff } /* Literal.Number.Integer */ - .mo { color: #ae81ff } /* Literal.Number.Oct */ - .sb { color: #e6db74 } /* Literal.String.Backtick */ - .sc { color: #e6db74 } /* Literal.String.Char */ - .sd { color: #e6db74 } /* Literal.String.Doc */ - .s2 { color: #e6db74 } /* Literal.String.Double */ - .se { color: #ae81ff } /* Literal.String.Escape */ - .sh { color: #e6db74 } /* Literal.String.Heredoc */ - .si { color: #e6db74 } /* Literal.String.Interpol */ - .sx { color: #e6db74 } /* Literal.String.Other */ - .sr { color: #e6db74 } /* Literal.String.Regex */ - .s1 { color: #e6db74 } /* Literal.String.Single */ - .ss { color: #e6db74 } /* Literal.String.Symbol */ - .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ - .vc { color: #f8f8f2 } /* Name.Variable.Class */ - .vg { color: #f8f8f2 } /* Name.Variable.Global */ - .vi { color: #f8f8f2 } /* Name.Variable.Instance */ - .il { color: #ae81ff } /* Literal.Number.Integer.Long */ + .hll { background-color: #49483e; } + .c { color: #75715e; } /* Comment */ + .err { color: #960050; background-color: #1e0010; } /* Error */ + .k { color: #66d9ef; } /* Keyword */ + .l { color: #ae81ff; } /* Literal */ + .n { color: #f8f8f2; } /* Name */ + .o { color: #f92672; } /* Operator */ + .p { color: #f8f8f2; } /* Punctuation */ + .cm { color: #75715e; } /* Comment.Multiline */ + .cp { color: #75715e; } /* Comment.Preproc */ + .c1 { color: #75715e; } /* Comment.Single */ + .cs { color: #75715e; } /* Comment.Special */ + .ge { font-style: italic; } /* Generic.Emph */ + .gs { font-weight: bold; } /* Generic.Strong */ + .kc { color: #66d9ef; } /* Keyword.Constant */ + .kd { color: #66d9ef; } /* Keyword.Declaration */ + .kn { color: #f92672; } /* Keyword.Namespace */ + .kp { color: #66d9ef; } /* Keyword.Pseudo */ + .kr { color: #66d9ef; } /* Keyword.Reserved */ + .kt { color: #66d9ef; } /* Keyword.Type */ + .ld { color: #e6db74; } /* Literal.Date */ + .m { color: #ae81ff; } /* Literal.Number */ + .s { color: #e6db74; } /* Literal.String */ + .na { color: #a6e22e; } /* Name.Attribute */ + .nb { color: #f8f8f2; } /* Name.Builtin */ + .nc { color: #a6e22e; } /* Name.Class */ + .no { color: #66d9ef; } /* Name.Constant */ + .nd { color: #a6e22e; } /* Name.Decorator */ + .ni { color: #f8f8f2; } /* Name.Entity */ + .ne { color: #a6e22e; } /* Name.Exception */ + .nf { color: #a6e22e; } /* Name.Function */ + .nl { color: #f8f8f2; } /* Name.Label */ + .nn { color: #f8f8f2; } /* Name.Namespace */ + .nx { color: #a6e22e; } /* Name.Other */ + .py { color: #f8f8f2; } /* Name.Property */ + .nt { color: #f92672; } /* Name.Tag */ + .nv { color: #f8f8f2; } /* Name.Variable */ + .ow { color: #f92672; } /* Operator.Word */ + .w { color: #f8f8f2; } /* Text.Whitespace */ + .mf { color: #ae81ff; } /* Literal.Number.Float */ + .mh { color: #ae81ff; } /* Literal.Number.Hex */ + .mi { color: #ae81ff; } /* Literal.Number.Integer */ + .mo { color: #ae81ff; } /* Literal.Number.Oct */ + .sb { color: #e6db74; } /* Literal.String.Backtick */ + .sc { color: #e6db74; } /* Literal.String.Char */ + .sd { color: #e6db74; } /* Literal.String.Doc */ + .s2 { color: #e6db74; } /* Literal.String.Double */ + .se { color: #ae81ff; } /* Literal.String.Escape */ + .sh { color: #e6db74; } /* Literal.String.Heredoc */ + .si { color: #e6db74; } /* Literal.String.Interpol */ + .sx { color: #e6db74; } /* Literal.String.Other */ + .sr { color: #e6db74; } /* Literal.String.Regex */ + .s1 { color: #e6db74; } /* Literal.String.Single */ + .ss { color: #e6db74; } /* Literal.String.Symbol */ + .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */ + .vc { color: #f8f8f2; } /* Name.Variable.Class */ + .vg { color: #f8f8f2; } /* Name.Variable.Global */ + .vi { color: #f8f8f2; } /* Name.Variable.Instance */ + .il { color: #ae81ff; } /* Literal.Number.Integer.Long */ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index b11499c71ee..c3c7773b9e2 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -72,72 +72,72 @@ green #859900 operators, other keywords */ - .c { color: #586e75 } /* Comment */ - .err { color: #93a1a1 } /* Error */ - .g { color: #93a1a1 } /* Generic */ - .k { color: #859900 } /* Keyword */ - .l { color: #93a1a1 } /* Literal */ - .n { color: #93a1a1 } /* Name */ - .o { color: #859900 } /* Operator */ - .x { color: #cb4b16 } /* Other */ - .p { color: #93a1a1 } /* Punctuation */ - .cm { color: #586e75 } /* Comment.Multiline */ - .cp { color: #859900 } /* Comment.Preproc */ - .c1 { color: #586e75 } /* Comment.Single */ - .cs { color: #859900 } /* Comment.Special */ - .gd { color: #2aa198 } /* Generic.Deleted */ - .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ - .gr { color: #dc322f } /* Generic.Error */ - .gh { color: #cb4b16 } /* Generic.Heading */ - .gi { color: #859900 } /* Generic.Inserted */ - .go { color: #93a1a1 } /* Generic.Output */ - .gp { color: #93a1a1 } /* Generic.Prompt */ - .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ - .gu { color: #cb4b16 } /* Generic.Subheading */ - .gt { color: #93a1a1 } /* Generic.Traceback */ - .kc { color: #cb4b16 } /* Keyword.Constant */ - .kd { color: #268bd2 } /* Keyword.Declaration */ - .kn { color: #859900 } /* Keyword.Namespace */ - .kp { color: #859900 } /* Keyword.Pseudo */ - .kr { color: #268bd2 } /* Keyword.Reserved */ - .kt { color: #dc322f } /* Keyword.Type */ - .ld { color: #93a1a1 } /* Literal.Date */ - .m { color: #2aa198 } /* Literal.Number */ - .s { color: #2aa198 } /* Literal.String */ - .na { color: #93a1a1 } /* Name.Attribute */ - .nb { color: #b58900 } /* Name.Builtin */ - .nc { color: #268bd2 } /* Name.Class */ - .no { color: #cb4b16 } /* Name.Constant */ - .nd { color: #268bd2 } /* Name.Decorator */ - .ni { color: #cb4b16 } /* Name.Entity */ - .ne { color: #cb4b16 } /* Name.Exception */ - .nf { color: #268bd2 } /* Name.Function */ - .nl { color: #93a1a1 } /* Name.Label */ - .nn { color: #93a1a1 } /* Name.Namespace */ - .nx { color: #93a1a1 } /* Name.Other */ - .py { color: #93a1a1 } /* Name.Property */ - .nt { color: #268bd2 } /* Name.Tag */ - .nv { color: #268bd2 } /* Name.Variable */ - .ow { color: #859900 } /* Operator.Word */ - .w { color: #93a1a1 } /* Text.Whitespace */ - .mf { color: #2aa198 } /* Literal.Number.Float */ - .mh { color: #2aa198 } /* Literal.Number.Hex */ - .mi { color: #2aa198 } /* Literal.Number.Integer */ - .mo { color: #2aa198 } /* Literal.Number.Oct */ - .sb { color: #586e75 } /* Literal.String.Backtick */ - .sc { color: #2aa198 } /* Literal.String.Char */ - .sd { color: #93a1a1 } /* Literal.String.Doc */ - .s2 { color: #2aa198 } /* Literal.String.Double */ - .se { color: #cb4b16 } /* Literal.String.Escape */ - .sh { color: #93a1a1 } /* Literal.String.Heredoc */ - .si { color: #2aa198 } /* Literal.String.Interpol */ - .sx { color: #2aa198 } /* Literal.String.Other */ - .sr { color: #dc322f } /* Literal.String.Regex */ - .s1 { color: #2aa198 } /* Literal.String.Single */ - .ss { color: #2aa198 } /* Literal.String.Symbol */ - .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2 } /* Name.Variable.Class */ - .vg { color: #268bd2 } /* Name.Variable.Global */ - .vi { color: #268bd2 } /* Name.Variable.Instance */ - .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + .c { color: #586e75; } /* Comment */ + .err { color: #93a1a1; } /* Error */ + .g { color: #93a1a1; } /* Generic */ + .k { color: #859900; } /* Keyword */ + .l { color: #93a1a1; } /* Literal */ + .n { color: #93a1a1; } /* Name */ + .o { color: #859900; } /* Operator */ + .x { color: #cb4b16; } /* Other */ + .p { color: #93a1a1; } /* Punctuation */ + .cm { color: #586e75; } /* Comment.Multiline */ + .cp { color: #859900; } /* Comment.Preproc */ + .c1 { color: #586e75; } /* Comment.Single */ + .cs { color: #859900; } /* Comment.Special */ + .gd { color: #2aa198; } /* Generic.Deleted */ + .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */ + .gr { color: #dc322f; } /* Generic.Error */ + .gh { color: #cb4b16; } /* Generic.Heading */ + .gi { color: #859900; } /* Generic.Inserted */ + .go { color: #93a1a1; } /* Generic.Output */ + .gp { color: #93a1a1; } /* Generic.Prompt */ + .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */ + .gu { color: #cb4b16; } /* Generic.Subheading */ + .gt { color: #93a1a1; } /* Generic.Traceback */ + .kc { color: #cb4b16; } /* Keyword.Constant */ + .kd { color: #268bd2; } /* Keyword.Declaration */ + .kn { color: #859900; } /* Keyword.Namespace */ + .kp { color: #859900; } /* Keyword.Pseudo */ + .kr { color: #268bd2; } /* Keyword.Reserved */ + .kt { color: #dc322f; } /* Keyword.Type */ + .ld { color: #93a1a1; } /* Literal.Date */ + .m { color: #2aa198; } /* Literal.Number */ + .s { color: #2aa198; } /* Literal.String */ + .na { color: #93a1a1; } /* Name.Attribute */ + .nb { color: #b58900; } /* Name.Builtin */ + .nc { color: #268bd2; } /* Name.Class */ + .no { color: #cb4b16; } /* Name.Constant */ + .nd { color: #268bd2; } /* Name.Decorator */ + .ni { color: #cb4b16; } /* Name.Entity */ + .ne { color: #cb4b16; } /* Name.Exception */ + .nf { color: #268bd2; } /* Name.Function */ + .nl { color: #93a1a1; } /* Name.Label */ + .nn { color: #93a1a1; } /* Name.Namespace */ + .nx { color: #93a1a1; } /* Name.Other */ + .py { color: #93a1a1; } /* Name.Property */ + .nt { color: #268bd2; } /* Name.Tag */ + .nv { color: #268bd2; } /* Name.Variable */ + .ow { color: #859900; } /* Operator.Word */ + .w { color: #93a1a1; } /* Text.Whitespace */ + .mf { color: #2aa198; } /* Literal.Number.Float */ + .mh { color: #2aa198; } /* Literal.Number.Hex */ + .mi { color: #2aa198; } /* Literal.Number.Integer */ + .mo { color: #2aa198; } /* Literal.Number.Oct */ + .sb { color: #586e75; } /* Literal.String.Backtick */ + .sc { color: #2aa198; } /* Literal.String.Char */ + .sd { color: #93a1a1; } /* Literal.String.Doc */ + .s2 { color: #2aa198; } /* Literal.String.Double */ + .se { color: #cb4b16; } /* Literal.String.Escape */ + .sh { color: #93a1a1; } /* Literal.String.Heredoc */ + .si { color: #2aa198; } /* Literal.String.Interpol */ + .sx { color: #2aa198; } /* Literal.String.Other */ + .sr { color: #dc322f; } /* Literal.String.Regex */ + .s1 { color: #2aa198; } /* Literal.String.Single */ + .ss { color: #2aa198; } /* Literal.String.Symbol */ + .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2; } /* Name.Variable.Class */ + .vg { color: #268bd2; } /* Name.Variable.Global */ + .vi { color: #268bd2; } /* Name.Variable.Instance */ + .il { color: #2aa198; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 657bb5e3cd9..5956a28cafe 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -78,72 +78,72 @@ green #859900 operators, other keywords */ - .c { color: #93a1a1 } /* Comment */ - .err { color: #586e75 } /* Error */ - .g { color: #586e75 } /* Generic */ - .k { color: #859900 } /* Keyword */ - .l { color: #586e75 } /* Literal */ - .n { color: #586e75 } /* Name */ - .o { color: #859900 } /* Operator */ - .x { color: #cb4b16 } /* Other */ - .p { color: #586e75 } /* Punctuation */ - .cm { color: #93a1a1 } /* Comment.Multiline */ - .cp { color: #859900 } /* Comment.Preproc */ - .c1 { color: #93a1a1 } /* Comment.Single */ - .cs { color: #859900 } /* Comment.Special */ - .gd { color: #2aa198 } /* Generic.Deleted */ - .ge { color: #586e75; font-style: italic } /* Generic.Emph */ - .gr { color: #dc322f } /* Generic.Error */ - .gh { color: #cb4b16 } /* Generic.Heading */ - .gi { color: #859900 } /* Generic.Inserted */ - .go { color: #586e75 } /* Generic.Output */ - .gp { color: #586e75 } /* Generic.Prompt */ - .gs { color: #586e75; font-weight: bold } /* Generic.Strong */ - .gu { color: #cb4b16 } /* Generic.Subheading */ - .gt { color: #586e75 } /* Generic.Traceback */ - .kc { color: #cb4b16 } /* Keyword.Constant */ - .kd { color: #268bd2 } /* Keyword.Declaration */ - .kn { color: #859900 } /* Keyword.Namespace */ - .kp { color: #859900 } /* Keyword.Pseudo */ - .kr { color: #268bd2 } /* Keyword.Reserved */ - .kt { color: #dc322f } /* Keyword.Type */ - .ld { color: #586e75 } /* Literal.Date */ - .m { color: #2aa198 } /* Literal.Number */ - .s { color: #2aa198 } /* Literal.String */ - .na { color: #586e75 } /* Name.Attribute */ - .nb { color: #b58900 } /* Name.Builtin */ - .nc { color: #268bd2 } /* Name.Class */ - .no { color: #cb4b16 } /* Name.Constant */ - .nd { color: #268bd2 } /* Name.Decorator */ - .ni { color: #cb4b16 } /* Name.Entity */ - .ne { color: #cb4b16 } /* Name.Exception */ - .nf { color: #268bd2 } /* Name.Function */ - .nl { color: #586e75 } /* Name.Label */ - .nn { color: #586e75 } /* Name.Namespace */ - .nx { color: #586e75 } /* Name.Other */ - .py { color: #586e75 } /* Name.Property */ - .nt { color: #268bd2 } /* Name.Tag */ - .nv { color: #268bd2 } /* Name.Variable */ - .ow { color: #859900 } /* Operator.Word */ - .w { color: #586e75 } /* Text.Whitespace */ - .mf { color: #2aa198 } /* Literal.Number.Float */ - .mh { color: #2aa198 } /* Literal.Number.Hex */ - .mi { color: #2aa198 } /* Literal.Number.Integer */ - .mo { color: #2aa198 } /* Literal.Number.Oct */ - .sb { color: #93a1a1 } /* Literal.String.Backtick */ - .sc { color: #2aa198 } /* Literal.String.Char */ - .sd { color: #586e75 } /* Literal.String.Doc */ - .s2 { color: #2aa198 } /* Literal.String.Double */ - .se { color: #cb4b16 } /* Literal.String.Escape */ - .sh { color: #586e75 } /* Literal.String.Heredoc */ - .si { color: #2aa198 } /* Literal.String.Interpol */ - .sx { color: #2aa198 } /* Literal.String.Other */ - .sr { color: #dc322f } /* Literal.String.Regex */ - .s1 { color: #2aa198 } /* Literal.String.Single */ - .ss { color: #2aa198 } /* Literal.String.Symbol */ - .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ - .vc { color: #268bd2 } /* Name.Variable.Class */ - .vg { color: #268bd2 } /* Name.Variable.Global */ - .vi { color: #268bd2 } /* Name.Variable.Instance */ - .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + .c { color: #93a1a1; } /* Comment */ + .err { color: #586e75; } /* Error */ + .g { color: #586e75; } /* Generic */ + .k { color: #859900; } /* Keyword */ + .l { color: #586e75; } /* Literal */ + .n { color: #586e75; } /* Name */ + .o { color: #859900; } /* Operator */ + .x { color: #cb4b16; } /* Other */ + .p { color: #586e75; } /* Punctuation */ + .cm { color: #93a1a1; } /* Comment.Multiline */ + .cp { color: #859900; } /* Comment.Preproc */ + .c1 { color: #93a1a1; } /* Comment.Single */ + .cs { color: #859900; } /* Comment.Special */ + .gd { color: #2aa198; } /* Generic.Deleted */ + .ge { color: #586e75; font-style: italic; } /* Generic.Emph */ + .gr { color: #dc322f; } /* Generic.Error */ + .gh { color: #cb4b16; } /* Generic.Heading */ + .gi { color: #859900; } /* Generic.Inserted */ + .go { color: #586e75; } /* Generic.Output */ + .gp { color: #586e75; } /* Generic.Prompt */ + .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */ + .gu { color: #cb4b16; } /* Generic.Subheading */ + .gt { color: #586e75; } /* Generic.Traceback */ + .kc { color: #cb4b16; } /* Keyword.Constant */ + .kd { color: #268bd2; } /* Keyword.Declaration */ + .kn { color: #859900; } /* Keyword.Namespace */ + .kp { color: #859900; } /* Keyword.Pseudo */ + .kr { color: #268bd2; } /* Keyword.Reserved */ + .kt { color: #dc322f; } /* Keyword.Type */ + .ld { color: #586e75; } /* Literal.Date */ + .m { color: #2aa198; } /* Literal.Number */ + .s { color: #2aa198; } /* Literal.String */ + .na { color: #586e75; } /* Name.Attribute */ + .nb { color: #b58900; } /* Name.Builtin */ + .nc { color: #268bd2; } /* Name.Class */ + .no { color: #cb4b16; } /* Name.Constant */ + .nd { color: #268bd2; } /* Name.Decorator */ + .ni { color: #cb4b16; } /* Name.Entity */ + .ne { color: #cb4b16; } /* Name.Exception */ + .nf { color: #268bd2; } /* Name.Function */ + .nl { color: #586e75; } /* Name.Label */ + .nn { color: #586e75; } /* Name.Namespace */ + .nx { color: #586e75; } /* Name.Other */ + .py { color: #586e75; } /* Name.Property */ + .nt { color: #268bd2; } /* Name.Tag */ + .nv { color: #268bd2; } /* Name.Variable */ + .ow { color: #859900; } /* Operator.Word */ + .w { color: #586e75; } /* Text.Whitespace */ + .mf { color: #2aa198; } /* Literal.Number.Float */ + .mh { color: #2aa198; } /* Literal.Number.Hex */ + .mi { color: #2aa198; } /* Literal.Number.Integer */ + .mo { color: #2aa198; } /* Literal.Number.Oct */ + .sb { color: #93a1a1; } /* Literal.String.Backtick */ + .sc { color: #2aa198; } /* Literal.String.Char */ + .sd { color: #586e75; } /* Literal.String.Doc */ + .s2 { color: #2aa198; } /* Literal.String.Double */ + .se { color: #cb4b16; } /* Literal.String.Escape */ + .sh { color: #586e75; } /* Literal.String.Heredoc */ + .si { color: #2aa198; } /* Literal.String.Interpol */ + .sx { color: #2aa198; } /* Literal.String.Other */ + .sr { color: #dc322f; } /* Literal.String.Regex */ + .s1 { color: #2aa198; } /* Literal.String.Single */ + .ss { color: #2aa198; } /* Literal.String.Symbol */ + .bp { color: #268bd2; } /* Name.Builtin.Pseudo */ + .vc { color: #268bd2; } /* Name.Variable.Class */ + .vg { color: #268bd2; } /* Name.Variable.Global */ + .vi { color: #268bd2; } /* Name.Variable.Instance */ + .il { color: #2aa198; } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 36a80a916b2..6f31a5235c0 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -86,7 +86,7 @@ background-color: #fafe3d !important; } - .hll { background-color: #f8f8f8 } + .hll { background-color: #f8f8f8; } .c { color: #998; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } .k { font-weight: bold; } diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss index 5bfe9bcb443..8d1a6020ca4 100644 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -78,7 +78,7 @@ span.highlight_word { background-color: #fafe3d !important; } -.hll { background-color: #f8f8f8 } +.hll { background-color: #f8f8f8; } .c { color: #998; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } .k { font-weight: bold; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index fc12964872d..ced8c4a9907 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -2,22 +2,28 @@ img { max-width: 100%; height: auto; } + p.details { font-style: italic; - color: #777 + color: #777; } + .footer > p { font-size: small; - color: #777 + color: #777; } + pre.commit-message { white-space: pre-wrap; } + .file-stats > a { text-decoration: none; + > .new-file { color: #090; } + > .deleted-file { color: #b00; } diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 8f71381f5c4..140d589024b 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -22,7 +22,7 @@ .admin-filter form { .select2-container { - width: 100% + width: 100%; } .controls { @@ -31,7 +31,7 @@ .form-actions { padding-left: 130px; - background: #fff + background: #fff; } .visibility-levels { @@ -106,26 +106,33 @@ .table { table-layout: fixed; } + .subheading { padding-bottom: $gl-padding; } + .message { word-wrap: break-word; } + .btn { white-space: normal; padding: $gl-btn-padding; } + th { width: 15%; + &.wide { width: 55%; } } + @media (max-width: $screen-sm-max) { th { width: 100%; } + td { width: 100%; float: left; @@ -137,6 +144,7 @@ margin-left: $btn-side-margin; margin-top: 3px; } + span { font-size: 19px; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 194a39a8377..2fbf0cf34bf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -137,6 +137,7 @@ .retry-link { color: $gl-link-color; + &:hover { text-decoration: underline; } @@ -218,6 +219,7 @@ .build-detail-row { margin-bottom: 5px; + &:last-of-type { margin-bottom: 0; } @@ -233,3 +235,9 @@ right: 0; margin-top: -17px; } + +@media (min-width: $screen-md-min) { + .sub-nav.build { + width: calc(100% + #{$gutter_width}); + } +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 53ec0002afe..264e7e01a34 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -51,6 +51,7 @@ margin-left: 4px; } } + .commit-committer-link, .commit-author-link { color: $gl-gray; @@ -108,21 +109,25 @@ line-height: 20px; } } + .new-file { a { color: $gl-text-green; } } + .renamed-file { a { color: $gl-text-orange; } } + .deleted-file { a { color: $gl-text-red; } } + .edit-file { a { color: $gl-text-color; @@ -158,6 +163,7 @@ position: absolute; z-index: 1; } + > textarea { background-color: rgba(0, 0, 0, 0.0); font-family: inherit; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index dc57a837155..2b5621e20d6 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -161,6 +161,7 @@ .branch-commit { color: $gl-gray; + .commit-id, .commit-row-message { color: $gl-gray; } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 42928ee279c..76225ed8d06 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -5,6 +5,7 @@ background: $background-color; border-top-left-radius: 0; } + border-top-left-radius: 0; } } @@ -17,6 +18,7 @@ float: left; @extend .col-md-2; } + .btn { margin-left: 5px; float: left; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b8ef76cc74e..bdc82a8f0f5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -33,6 +33,19 @@ font-size: smaller; } } + + .file-title { + cursor: pointer; + + &:hover { + background-color: $dark-background-color; + } + + .diff-toggle-caret { + padding-right: 6px; + } + } + .diff-content { overflow: auto; overflow-y: hidden; @@ -123,15 +136,18 @@ max-width: 50px; width: 35px; @include user-select(none); + a { float: left; width: 35px; font-weight: normal; + &:hover { text-decoration: underline; } } } + .line_content { display: block; margin: 0; @@ -151,10 +167,12 @@ white-space: pre-wrap; } } + .image { background: #ddd; text-align: center; padding: 30px; + .wrap { display: inline-block; } @@ -163,6 +181,7 @@ display: inline-block; background-color: #fff; line-height: 0; + img { border: 1px solid #fff; background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%), @@ -171,6 +190,7 @@ background-position: 0 0, 5px 5px; max-width: 100%; } + &.deleted { border: 1px solid $deleted; } @@ -179,6 +199,7 @@ border: 1px solid $added; } } + .image-info { font-size: 12px; margin: 5px 0 0; @@ -193,6 +214,7 @@ margin: auto; position: relative; } + .swipe-wrap { overflow: hidden; border-left: 1px solid #999; @@ -201,10 +223,12 @@ top: 13px; right: 7px; } + .frame { top: 0; right: 0; position: absolute; + &.deleted { margin: 0; display: block; @@ -212,6 +236,7 @@ right: 7px; } } + .swipe-bar { display: block; height: 100%; @@ -219,14 +244,17 @@ z-index: 100; position: absolute; cursor: pointer; + &:hover { .top-handle { background-position: -15px 3px; } + .bottom-handle { background-position: -15px -11px; } } + .top-handle { display: block; height: 14px; @@ -235,6 +263,7 @@ top: 0; background: image-url('swipemode_sprites.gif') 0 3px no-repeat; } + .bottom-handle { display: block; height: 14px; @@ -252,12 +281,14 @@ margin: auto; position: relative; } + .frame.added, .frame.deleted { position: absolute; display: block; top: 0; left: 0; } + .controls { display: block; height: 14px; @@ -311,6 +342,7 @@ } //.view.onion-skin } + .view-modes { padding: 10px; text-align: center; @@ -328,19 +360,24 @@ border-left: 1px solid #c1c1c1; padding: 0 12px 0 16px; cursor: pointer; + &:first-child { border-left: none; } + &:hover { text-decoration: underline; } + &.active { &:hover { text-decoration: none; } + cursor: default; color: #333; } + &.disabled { display: none; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index fcc5f32c738..029dabd2138 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -15,6 +15,7 @@ .cancel-btn { color: #b94a48; + &:hover { color: #b94a48; } @@ -70,16 +71,20 @@ .soft-wrap-toggle { margin: 0 $btn-side-margin; + .soft-wrap { display: block; } + .no-wrap { display: none; } + &.soft-wrap-active { .soft-wrap { display: none; } + .no-wrap { display: block; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3f19e920166..820cc0fc991 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -52,13 +52,13 @@ } } -.table.builds.environments { +.table.ci-table.environments { .icon-container { width: 20px; text-align: center; } - + .branch-commit { .commit-id { margin-right: 0; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 789d6237df8..5d9a76dac05 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -78,6 +78,7 @@ margin-bottom: 0; } } + .event-note-icon { color: #777; float: left; @@ -86,6 +87,7 @@ margin-right: 5px; } } + .event_icon { position: relative; float: right; @@ -95,12 +97,13 @@ background: $gray-light; margin-left: 10px; top: -6px; + img { width: 20px; } } - &:last-child { border: none } + &:last-child { border: none; } .event_commits { li { @@ -109,6 +112,7 @@ padding: 3px; padding-left: 0; border: none; + .commit-row-title { font-size: $gl-font-size; } @@ -117,6 +121,7 @@ &.commits-stat { display: block; padding: 0 3px 0 0; + &:hover { background: none; } @@ -158,6 +163,7 @@ overflow: visible; max-width: 100%; } + .avatar { display: none; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 185ce970e71..ee2a398f031 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,17 +1,3 @@ -.member-search-form { - float: left; - - input[type='search'] { - width: 225px; - vertical-align: bottom; - - @media (max-width: $screen-xs-max) { - width: 100px; - vertical-align: bottom; - } - } -} - .milestone-row { @include str-truncated(90%); } @@ -48,6 +34,7 @@ .group-right-buttons { position: absolute; right: 16px; + .btn { @include btn-gray; padding: 3px 10px; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 00ab42bec5c..a48b4c65db8 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -23,28 +23,28 @@ color: #555; tbody:first-child tr:first-child { - padding-top: 0 + padding-top: 0; } th { padding-top: 15px; line-height: 1.5; color: #333; - text-align: left + text-align: left; } td { padding-top: 3px; padding-bottom: 3px; vertical-align: top; - line-height: 20px + line-height: 20px; } .shortcut { padding-right: 10px; color: #999; text-align: right; - white-space: nowrap + white-space: nowrap; } .key { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 41079b6eeb5..230b927a17d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -27,6 +27,7 @@ margin-right: 5px; margin-bottom: 5px; display: inline-block; + .color-label { padding: 6px 10px; } @@ -128,7 +129,7 @@ } .selectbox { - display: none + display: none; } .btn-clipboard { @@ -199,7 +200,7 @@ display: none; /* Small devices (tablets, 768px and up) */ @media (min-width: $screen-sm-min) { - display: block + display: block; } width: $sidebar_collapsed_width; @@ -276,7 +277,7 @@ } &.btn-primary { - @extend .btn-primary + @extend .btn-primary; } } @@ -400,6 +401,7 @@ .js-issuable-selector { width: 100%; } + @media (max-width: $screen-sm-max) { margin-bottom: $gl-padding; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3ac34cbc829..623da67a239 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -37,6 +37,7 @@ ul.related-merge-requests > li { display: -ms-flexbox; display: -webkit-flex; display: flex; + .merge-request-id { flex-shrink: 0; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 701c29a3986..9bac6d46355 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -1,5 +1,6 @@ .suggest-colors { margin-top: 5px; + a { border-radius: 4px; width: 30px; diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6926448519e..8290519dc25 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -3,6 +3,7 @@ font-size: 19px; color: red; } + .correct-syntax { font-size: 19px; color: #47a447; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index a5ca509163d..4c0c0839fe2 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -17,6 +17,7 @@ line-height: 1.5; p { + font-size: 18px; color: #888; } @@ -36,10 +37,14 @@ } } - .login-box { - background: #fafafa; - border-radius: 10px; - box-shadow: 0 0 2px #ccc; + p { + font-size: 13px; + } + + .login-box, .omniauth-container { + box-shadow: 0 0 0 1px $border-color; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 2px; padding: 15px; .login-heading h3 { @@ -58,42 +63,127 @@ a.forgot { float: right; - padding-top: 6px + padding-top: 6px; } .nav .active a { background: transparent; } - } - .form-control { - font-size: 14px; - padding: 10px 8px; - width: 100%; - height: auto; - - &.top { - border-radius: 5px 5px 0 0; - margin-bottom: 0; + // Styles the glowing border of focused input for username async validation + .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 { + color: $green-normal; + } + + .username .validation-error, + .gl-field-error-message { + color: $red-normal; + } + + .gl-field-hint { + color: $gl-text-color; + } + } + } - &.bottom { - border-radius: 0 0 5px 5px; - border-top: 0; - margin-bottom: 20px; + .omniauth-container { + p { + margin: 0; } + } + + .new-session-tabs { + display: -webkit-flex; + display: flex; + box-shadow: 0 0 0 1px $border-color; + border-top-right-radius: 2px; + border-top-left-radius: 2px; + + li { + flex: 1; + text-align: center; + + &:last-of-type { + border-left: 1px solid $border-color; + } + + &:not(.active) { + background-color: $gray-light; + } + + a { + width: 100%; + font-size: 18px; - &.middle { - border-top: 0; - margin-bottom: 0; - border-radius: 0; + &:hover { + border: 1px solid transparent; + } + } + + &.active { + border-bottom: 1px solid $border-color; + + a { + border: none; + border-bottom: 2px solid $link-underline-blue; + color: $black; + + &:hover { + border-bottom: 2px solid $link-underline-blue; + } + } + } } + } + + .form-control { &:active, &:focus { background-color: #fff; } } + label { + font-weight: normal; + } + + .submit-container { + margin-top: 16px; + } + + input[type="submit"] { + @extend .btn-block; + margin-bottom: 0; + } + .devise-errors { h2 { margin-top: 0; @@ -101,14 +191,6 @@ color: #a00; } } - - .remember-me { - margin-top: -10px; - - label { - font-weight: normal; - } - } } @media (max-width: $screen-xs-max) { @@ -127,3 +209,35 @@ height: 32px; } } + +.devise-layout-html { + margin: 0; + padding: 0; + height: 100%; +} + +// Fixes footer container to bottom of viewport +.devise-layout-html body { + // offset height of fixed header + 1 to avoid scroll + height: calc(100% - 51px); + margin: 0; + padding: 0; + + .page-wrap { + min-height: 100%; + position: relative; + } + + .footer-container, hr.footer-fixed { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40px; + background: $white-light; + } + + .navless-container { + padding: 65px; // height of footer + bottom padding of email confirmation link + } +} diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss new file mode 100644 index 00000000000..756efa9c7fa --- /dev/null +++ b/app/assets/stylesheets/pages/members.scss @@ -0,0 +1,98 @@ +.project-members-title { + padding-bottom: 10px; + border-bottom: 1px solid $border-color; +} + +.member { + .list-item-name { + @media (min-width: $screen-sm-min) { + float: left; + width: 50%; + } + + strong { + font-weight: 600; + } + } + + .controls { + @media (min-width: $screen-sm-min) { + display: -webkit-flex; + display: flex; + width: 400px; + max-width: 50%; + } + } + + .form-horizontal { + margin-top: 5px; + + @media (min-width: $screen-sm-min) { + display: -webkit-flex; + display: flex; + width: 100%; + margin-top: 3px; + } + } + + .btn-remove { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: auto; + } + } +} + +.member-form-control { + @media (max-width: $screen-xs-max) { + padding: 5px 0; + margin-left: 0; + margin-right: 0; + } + + @media (min-width: $screen-sm-min) { + width: 50%; + } +} + +.member-access-text { + margin-left: auto; + line-height: 43px; +} + +.member.existing-title { + @media (min-width: $screen-sm-min) { + float: left; + } +} + +.member-search-form { + position: relative; + + @media (min-width: $screen-sm-min) { + float: right; + } + + .form-control { + width: 100%; + padding-right: 35px; + + @media (min-width: $screen-sm-min) { + width: 350px; + } + } +} + +.member-search-btn { + position: absolute; + right: 0; + top: 0; + height: 35px; + padding-left: 10px; + padding-right: 10px; + color: $gray-darkest; + background: transparent; + border: 0; + outline: 0; +} diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 5ec660799e3..49013d7cac9 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -131,6 +131,7 @@ $colors: ( } } } + &.head { background-color: map-get($colors, #{$color}_header_head_neutral); border-color: map-get($colors, #{$color}_header_head_neutral); @@ -174,6 +175,7 @@ $colors: ( background-color: map-get($colors, #{$color}_line_not_chosen); } } + &.head { background-color: map-get($colors, #{$color}_line_head_neutral); diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7cf69c56d15..afc4e517fde 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -10,6 +10,7 @@ form { margin-bottom: 0; + .clearfix { margin-bottom: 0; } @@ -46,6 +47,7 @@ &.right { float: right; + a { color: $gl-gray; } @@ -121,6 +123,10 @@ color: #5c5d5e; } + .js-deployment-link { + display: inline-block; + } + .mr-widget-body { h4 { font-weight: 600; @@ -188,6 +194,7 @@ padding-top: 2px; padding-bottom: 2px; list-style: none; + &:hover { background: none; } @@ -211,6 +218,7 @@ padding-top: 20px; padding-bottom: 10px; } + svg { width: 230px; } @@ -276,12 +284,6 @@ line-height: 31px; } -.builds { - .table-holder { - overflow-x: auto; - } -} - .panel-new-merge-request { .panel-heading { padding: 5px 10px; @@ -369,7 +371,7 @@ } .table-holder { - .builds { + .ci-table { th { background-color: $white-light; @@ -433,3 +435,12 @@ margin-bottom: 20px; } } + +.merge-request-tabs { + background-color: #fff; + + &.affix { + top: 100px; + z-index: 9; + } +} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 8c2ba3ed58c..dd6d1783667 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -59,6 +59,7 @@ color: $gl-placeholder-color; margin-right: 5px; } + .avatar { float: none; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index bd875b9823f..17f28959414 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -11,6 +11,7 @@ filter: alpha(opacity=100); } } + .diff-file, .discussion { .new-note { @@ -194,6 +195,7 @@ min-height: 140px; max-height: 500px; } + .note-form-actions { background: transparent; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index d399f84a2ff..fffcdc812a7 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -147,9 +147,18 @@ ul.notes { // Diff code in discussion view .discussion-body .diff-file { + .file-title { + cursor: default; + + &:hover { + background-color: $gray-light; + } + } + .diff-header > span { margin-right: 10px; } + .line_content { white-space: pre-wrap; } @@ -345,6 +354,7 @@ ul.notes { width: 32px; // "hide" it by default display: none; + &:hover { background: $gl-info; color: #fff; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7843355f0ab..7b71876b822 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -20,7 +20,7 @@ margin: 4px; } - .table.builds { + .table.ci-table { min-width: 1200px; .branch-commit { @@ -44,13 +44,20 @@ overflow: auto; } -.table.builds { +.table.ci-table { min-width: 900px; &.pipeline { min-width: 650px; } + &.builds-page { + + tr { + height: 71px; + } + } + tr { th { padding: 16px 8px; @@ -176,7 +183,7 @@ &::after { content: ''; width: 8px; - position: absolute;; + position: absolute; right: -7px; bottom: 8px; border-bottom: 2px solid $border-color; @@ -353,6 +360,7 @@ &:hover { background-color: $gray-lighter; + .dropdown-menu-toggle { background-color: transparent; } @@ -536,6 +544,7 @@ height: 29px; top: -9px; } + .curve { display: block; } @@ -621,19 +630,31 @@ } } -.pipelines.tab-pane { +.tab-pane { - .content-list.pipelines { - overflow: auto; - } + &.pipelines { - .stage { - max-width: 100px; - width: 100px; + .content-list.pipelines { + overflow: auto; + } + + .stage { + max-width: 100px; + width: 100px; + } + + .pipeline-actions { + min-width: initial; + } } - .pipeline-actions { - min-width: initial; + &.builds { + + .ci-table { + tr { + height: 71px; + } + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index c7eac5cf4b9..ed80d2beec2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -243,6 +243,7 @@ .btn { -webkit-flex-grow: 1; flex-grow: 1; + &:first-child { margin-left: 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 530fb0c0d05..d30f02340b9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -17,34 +17,43 @@ &.features .control-label { font-weight: normal; } + .form-group { margin-bottom: 5px; } + &> .form-group { padding-left: 0; } } + .help-block { margin-bottom: 10px; } + .project-path { padding-right: 0; + .form-control { border-radius: $border-radius-base; } } + .input-group > div { &:last-child { padding-right: 0; } } + @media (max-width: $screen-xs-max) { .input-group > div { margin-bottom: 14px; + &:last-child { margin-bottom: 0; } } + fieldset > .form-group:first-child { padding-right: 0; } @@ -56,6 +65,7 @@ border-radius: 3px; border: 1px solid #e5e5e5; } + &+ .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -201,6 +211,7 @@ pointer-events: none; } } + .count { @include btn-gray; display: inline-block; @@ -361,10 +372,12 @@ a.deploy-project-label { margin: $gl-padding; text-align: center; width: 169px; + &:hover, &.forked { background-color: $row-hover; border-color: $row-hover-border; } + .no-avatar { width: 100px; height: 100px; @@ -372,17 +385,20 @@ a.deploy-project-label { border: 1px solid $gray-dark; margin: 0 auto; border-radius: 50%; + i { font-size: 100px; color: $gray-dark; } } + a { display: block; width: 100%; height: 100%; padding-top: $gl-padding; color: $gl-gray; + .caption { min-height: 30px; padding: $gl-padding 0; @@ -644,6 +660,7 @@ pre.light-well { .clone-options { display: table-cell; + a.btn { width: 100%; } @@ -832,6 +849,7 @@ pre.light-well { .form-control { min-width: 100px; } + .select2-choice { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index eec22c5dc96..7b3878c91df 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -6,6 +6,7 @@ &.runner-state-shared { background: #32b186; } + &.runner-state-specific { background: #3498db; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index c05f3d5ff32..f1d53c7b8bc 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -65,6 +65,7 @@ .ci-status-icon-success { color: $gl-success; } + .ci-status-icon-failed { color: $gl-danger; } @@ -77,6 +78,7 @@ .ci-status-icon-running { color: $blue-normal; } + .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found, diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 41ad10f07bd..99c0f6362d0 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -5,6 +5,7 @@ .file-finder { width: 50%; + .file-finder-input { width: 95%; display: inline-block; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index c9846103762..3fa7fa3d7e3 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -23,15 +23,19 @@ .term-bold { font-weight: bold; } + .term-italic { font-style: italic; } + .term-conceal { visibility: hidden; } + .term-underline { text-decoration: underline; } + .term-cross { text-decoration: line-through; } @@ -39,48 +43,63 @@ .term-fg-black { color: $black; } + .term-fg-red { color: $red; } + .term-fg-green { color: $green; } + .term-fg-yellow { color: $yellow; } + .term-fg-blue { color: $blue; } + .term-fg-magenta { color: $magenta; } + .term-fg-cyan { color: $cyan; } + .term-fg-white { color: $white; } + .term-fg-l-black { color: $l-black; } + .term-fg-l-red { color: $l-red; } + .term-fg-l-green { color: $l-green; } + .term-fg-l-yellow { color: $l-yellow; } + .term-fg-l-blue { color: $l-blue; } + .term-fg-l-magenta { color: $l-magenta; } + .term-fg-l-cyan { color: $l-cyan; } + .term-fg-l-white { color: $l-white; } @@ -88,818 +107,1087 @@ .term-bg-black { background-color: $black; } + .term-bg-red { background-color: $red; } + .term-bg-green { background-color: $green; } + .term-bg-yellow { background-color: $yellow; } + .term-bg-blue { background-color: $blue; } + .term-bg-magenta { background-color: $magenta; } + .term-bg-cyan { background-color: $cyan; } + .term-bg-white { background-color: $white; } + .term-bg-l-black { background-color: $l-black; } + .term-bg-l-red { background-color: $l-red; } + .term-bg-l-green { background-color: $l-green; } + .term-bg-l-yellow { background-color: $l-yellow; } + .term-bg-l-blue { background-color: $l-blue; } + .term-bg-l-magenta { background-color: $l-magenta; } + .term-bg-l-cyan { background-color: $l-cyan; } + .term-bg-l-white { background-color: $l-white; } - .xterm-fg-0 { color: #000; } + .xterm-fg-1 { color: #800000; } + .xterm-fg-2 { color: #008000; } + .xterm-fg-3 { color: #808000; } + .xterm-fg-4 { color: #000080; } + .xterm-fg-5 { color: #800080; } + .xterm-fg-6 { color: #008080; } + .xterm-fg-7 { color: #c0c0c0; } + .xterm-fg-8 { color: #808080; } + .xterm-fg-9 { color: #f00; } + .xterm-fg-10 { color: #0f0; } + .xterm-fg-11 { color: #ff0; } + .xterm-fg-12 { color: #00f; } + .xterm-fg-13 { color: #f0f; } + .xterm-fg-14 { color: #0ff; } + .xterm-fg-15 { color: #fff; } + .xterm-fg-16 { color: #000; } + .xterm-fg-17 { color: #00005f; } + .xterm-fg-18 { color: #000087; } + .xterm-fg-19 { color: #0000af; } + .xterm-fg-20 { color: #0000d7; } + .xterm-fg-21 { color: #00f; } + .xterm-fg-22 { color: #005f00; } + .xterm-fg-23 { color: #005f5f; } + .xterm-fg-24 { color: #005f87; } + .xterm-fg-25 { color: #005faf; } + .xterm-fg-26 { color: #005fd7; } + .xterm-fg-27 { color: #005fff; } + .xterm-fg-28 { color: #008700; } + .xterm-fg-29 { color: #00875f; } + .xterm-fg-30 { color: #008787; } + .xterm-fg-31 { color: #0087af; } + .xterm-fg-32 { color: #0087d7; } + .xterm-fg-33 { color: #0087ff; } + .xterm-fg-34 { color: #00af00; } + .xterm-fg-35 { color: #00af5f; } + .xterm-fg-36 { color: #00af87; } + .xterm-fg-37 { color: #00afaf; } + .xterm-fg-38 { color: #00afd7; } + .xterm-fg-39 { color: #00afff; } + .xterm-fg-40 { color: #00d700; } + .xterm-fg-41 { color: #00d75f; } + .xterm-fg-42 { color: #00d787; } + .xterm-fg-43 { color: #00d7af; } + .xterm-fg-44 { color: #00d7d7; } + .xterm-fg-45 { color: #00d7ff; } + .xterm-fg-46 { color: #0f0; } + .xterm-fg-47 { color: #00ff5f; } + .xterm-fg-48 { color: #00ff87; } + .xterm-fg-49 { color: #00ffaf; } + .xterm-fg-50 { color: #00ffd7; } + .xterm-fg-51 { color: #0ff; } + .xterm-fg-52 { color: #5f0000; } + .xterm-fg-53 { color: #5f005f; } + .xterm-fg-54 { color: #5f0087; } + .xterm-fg-55 { color: #5f00af; } + .xterm-fg-56 { color: #5f00d7; } + .xterm-fg-57 { color: #5f00ff; } + .xterm-fg-58 { color: #5f5f00; } + .xterm-fg-59 { color: #5f5f5f; } + .xterm-fg-60 { color: #5f5f87; } + .xterm-fg-61 { color: #5f5faf; } + .xterm-fg-62 { color: #5f5fd7; } + .xterm-fg-63 { color: #5f5fff; } + .xterm-fg-64 { color: #5f8700; } + .xterm-fg-65 { color: #5f875f; } + .xterm-fg-66 { color: #5f8787; } + .xterm-fg-67 { color: #5f87af; } + .xterm-fg-68 { color: #5f87d7; } + .xterm-fg-69 { color: #5f87ff; } + .xterm-fg-70 { color: #5faf00; } + .xterm-fg-71 { color: #5faf5f; } + .xterm-fg-72 { color: #5faf87; } + .xterm-fg-73 { color: #5fafaf; } + .xterm-fg-74 { color: #5fafd7; } + .xterm-fg-75 { color: #5fafff; } + .xterm-fg-76 { color: #5fd700; } + .xterm-fg-77 { color: #5fd75f; } + .xterm-fg-78 { color: #5fd787; } + .xterm-fg-79 { color: #5fd7af; } + .xterm-fg-80 { color: #5fd7d7; } + .xterm-fg-81 { color: #5fd7ff; } + .xterm-fg-82 { color: #5fff00; } + .xterm-fg-83 { color: #5fff5f; } + .xterm-fg-84 { color: #5fff87; } + .xterm-fg-85 { color: #5fffaf; } + .xterm-fg-86 { color: #5fffd7; } + .xterm-fg-87 { color: #5fffff; } + .xterm-fg-88 { color: #870000; } + .xterm-fg-89 { color: #87005f; } + .xterm-fg-90 { color: #870087; } + .xterm-fg-91 { color: #8700af; } + .xterm-fg-92 { color: #8700d7; } + .xterm-fg-93 { color: #8700ff; } + .xterm-fg-94 { color: #875f00; } + .xterm-fg-95 { color: #875f5f; } + .xterm-fg-96 { color: #875f87; } + .xterm-fg-97 { color: #875faf; } + .xterm-fg-98 { color: #875fd7; } + .xterm-fg-99 { color: #875fff; } + .xterm-fg-100 { color: #878700; } + .xterm-fg-101 { color: #87875f; } + .xterm-fg-102 { color: #878787; } + .xterm-fg-103 { color: #8787af; } + .xterm-fg-104 { color: #8787d7; } + .xterm-fg-105 { color: #8787ff; } + .xterm-fg-106 { color: #87af00; } + .xterm-fg-107 { color: #87af5f; } + .xterm-fg-108 { color: #87af87; } + .xterm-fg-109 { color: #87afaf; } + .xterm-fg-110 { color: #87afd7; } + .xterm-fg-111 { color: #87afff; } + .xterm-fg-112 { color: #87d700; } + .xterm-fg-113 { color: #87d75f; } + .xterm-fg-114 { color: #87d787; } + .xterm-fg-115 { color: #87d7af; } + .xterm-fg-116 { color: #87d7d7; } + .xterm-fg-117 { color: #87d7ff; } + .xterm-fg-118 { color: #87ff00; } + .xterm-fg-119 { color: #87ff5f; } + .xterm-fg-120 { color: #87ff87; } + .xterm-fg-121 { color: #87ffaf; } + .xterm-fg-122 { color: #87ffd7; } + .xterm-fg-123 { color: #87ffff; } + .xterm-fg-124 { color: #af0000; } + .xterm-fg-125 { color: #af005f; } + .xterm-fg-126 { color: #af0087; } + .xterm-fg-127 { color: #af00af; } + .xterm-fg-128 { color: #af00d7; } + .xterm-fg-129 { color: #af00ff; } + .xterm-fg-130 { color: #af5f00; } + .xterm-fg-131 { color: #af5f5f; } + .xterm-fg-132 { color: #af5f87; } + .xterm-fg-133 { color: #af5faf; } + .xterm-fg-134 { color: #af5fd7; } + .xterm-fg-135 { color: #af5fff; } + .xterm-fg-136 { color: #af8700; } + .xterm-fg-137 { color: #af875f; } + .xterm-fg-138 { color: #af8787; } + .xterm-fg-139 { color: #af87af; } + .xterm-fg-140 { color: #af87d7; } + .xterm-fg-141 { color: #af87ff; } + .xterm-fg-142 { color: #afaf00; } + .xterm-fg-143 { color: #afaf5f; } + .xterm-fg-144 { color: #afaf87; } + .xterm-fg-145 { color: #afafaf; } + .xterm-fg-146 { color: #afafd7; } + .xterm-fg-147 { color: #afafff; } + .xterm-fg-148 { color: #afd700; } + .xterm-fg-149 { color: #afd75f; } + .xterm-fg-150 { color: #afd787; } + .xterm-fg-151 { color: #afd7af; } + .xterm-fg-152 { color: #afd7d7; } + .xterm-fg-153 { color: #afd7ff; } + .xterm-fg-154 { color: #afff00; } + .xterm-fg-155 { color: #afff5f; } + .xterm-fg-156 { color: #afff87; } + .xterm-fg-157 { color: #afffaf; } + .xterm-fg-158 { color: #afffd7; } + .xterm-fg-159 { color: #afffff; } + .xterm-fg-160 { color: #d70000; } + .xterm-fg-161 { color: #d7005f; } + .xterm-fg-162 { color: #d70087; } + .xterm-fg-163 { color: #d700af; } + .xterm-fg-164 { color: #d700d7; } + .xterm-fg-165 { color: #d700ff; } + .xterm-fg-166 { color: #d75f00; } + .xterm-fg-167 { color: #d75f5f; } + .xterm-fg-168 { color: #d75f87; } + .xterm-fg-169 { color: #d75faf; } + .xterm-fg-170 { color: #d75fd7; } + .xterm-fg-171 { color: #d75fff; } + .xterm-fg-172 { color: #d78700; } + .xterm-fg-173 { color: #d7875f; } + .xterm-fg-174 { color: #d78787; } + .xterm-fg-175 { color: #d787af; } + .xterm-fg-176 { color: #d787d7; } + .xterm-fg-177 { color: #d787ff; } + .xterm-fg-178 { color: #d7af00; } + .xterm-fg-179 { color: #d7af5f; } + .xterm-fg-180 { color: #d7af87; } + .xterm-fg-181 { color: #d7afaf; } + .xterm-fg-182 { color: #d7afd7; } + .xterm-fg-183 { color: #d7afff; } + .xterm-fg-184 { color: #d7d700; } + .xterm-fg-185 { color: #d7d75f; } + .xterm-fg-186 { color: #d7d787; } + .xterm-fg-187 { color: #d7d7af; } + .xterm-fg-188 { color: #d7d7d7; } + .xterm-fg-189 { color: #d7d7ff; } + .xterm-fg-190 { color: #d7ff00; } + .xterm-fg-191 { color: #d7ff5f; } + .xterm-fg-192 { color: #d7ff87; } + .xterm-fg-193 { color: #d7ffaf; } + .xterm-fg-194 { color: #d7ffd7; } + .xterm-fg-195 { color: #d7ffff; } + .xterm-fg-196 { color: #f00; } + .xterm-fg-197 { color: #ff005f; } + .xterm-fg-198 { color: #ff0087; } + .xterm-fg-199 { color: #ff00af; } + .xterm-fg-200 { color: #ff00d7; } + .xterm-fg-201 { color: #f0f; } + .xterm-fg-202 { color: #ff5f00; } + .xterm-fg-203 { color: #ff5f5f; } + .xterm-fg-204 { color: #ff5f87; } + .xterm-fg-205 { color: #ff5faf; } + .xterm-fg-206 { color: #ff5fd7; } + .xterm-fg-207 { color: #ff5fff; } + .xterm-fg-208 { color: #ff8700; } + .xterm-fg-209 { color: #ff875f; } + .xterm-fg-210 { color: #ff8787; } + .xterm-fg-211 { color: #ff87af; } + .xterm-fg-212 { color: #ff87d7; } + .xterm-fg-213 { color: #ff87ff; } + .xterm-fg-214 { color: #ffaf00; } + .xterm-fg-215 { color: #ffaf5f; } + .xterm-fg-216 { color: #ffaf87; } + .xterm-fg-217 { color: #ffafaf; } + .xterm-fg-218 { color: #ffafd7; } + .xterm-fg-219 { color: #ffafff; } + .xterm-fg-220 { color: #ffd700; } + .xterm-fg-221 { color: #ffd75f; } + .xterm-fg-222 { color: #ffd787; } + .xterm-fg-223 { color: #ffd7af; } + .xterm-fg-224 { color: #ffd7d7; } + .xterm-fg-225 { color: #ffd7ff; } + .xterm-fg-226 { color: #ff0; } + .xterm-fg-227 { color: #ffff5f; } + .xterm-fg-228 { color: #ffff87; } + .xterm-fg-229 { color: #ffffaf; } + .xterm-fg-230 { color: #ffffd7; } + .xterm-fg-231 { color: #fff; } + .xterm-fg-232 { color: #080808; } + .xterm-fg-233 { color: #121212; } + .xterm-fg-234 { color: #1c1c1c; } + .xterm-fg-235 { color: #262626; } + .xterm-fg-236 { color: #303030; } + .xterm-fg-237 { color: #3a3a3a; } + .xterm-fg-238 { color: #444; } + .xterm-fg-239 { color: #4e4e4e; } + .xterm-fg-240 { color: #585858; } + .xterm-fg-241 { color: #626262; } + .xterm-fg-242 { color: #6c6c6c; } + .xterm-fg-243 { color: #767676; } + .xterm-fg-244 { color: #808080; } + .xterm-fg-245 { color: #8a8a8a; } + .xterm-fg-246 { color: #949494; } + .xterm-fg-247 { color: #9e9e9e; } + .xterm-fg-248 { color: #a8a8a8; } + .xterm-fg-249 { color: #b2b2b2; } + .xterm-fg-250 { color: #bcbcbc; } + .xterm-fg-251 { color: #c6c6c6; } + .xterm-fg-252 { color: #d0d0d0; } + .xterm-fg-253 { color: #dadada; } + .xterm-fg-254 { color: #e4e4e4; } + .xterm-fg-255 { color: #eee; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b3455e04c29..705824502eb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -45,6 +45,10 @@ class ApplicationController < ActionController::Base redirect_to request.referer.present? ? :back : default, options end + def not_found + render_404 + end + protected # This filter handles both private tokens and personal access tokens diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 7a7475a7345..ae060abee5c 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -1,6 +1,7 @@ class Projects::GroupLinksController < Projects::ApplicationController layout 'project_settings' before_action :authorize_admin_project! + before_action :authorize_admin_project_member!, only: [:update] def index @group_links = project.project_group_links.all @@ -27,9 +28,26 @@ class Projects::GroupLinksController < Projects::ApplicationController redirect_to namespace_project_group_links_path(project.namespace, project) end + def update + @group_link = @project.project_group_links.find(params[:id]) + + @group_link.update_attributes(group_link_params) + end + def destroy project.project_group_links.find(params[:id]).destroy - redirect_to namespace_project_group_links_path(project.namespace, project) + respond_to do |format| + format.html do + redirect_to namespace_project_group_links_path(project.namespace, project) + end + format.js { head :ok } + end + end + + protected + + def group_link_params + params.require(:group_link).permit(:group_access, :expires_at) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 869d96b86f4..9207c954335 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, - :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] @@ -403,6 +403,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def ci_environments_status + environments = + begin + @merge_request.environments.map do |environment| + next unless can?(current_user, :read_environment, environment) + + project = environment.project + deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + + { + id: environment.id, + name: environment.name, + url: namespace_project_environment_path(project.namespace, project, environment), + external_url: environment.external_url, + external_url_formatted: environment.formatted_external_url, + deployed_at: deployment.try(:created_at), + deployed_at_formatted: deployment.try(:formatted_deployment_time) + } + end.compact + end + + render json: environments + end + protected def selected_target_project diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index f56b256984b..37a86ed0523 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -5,34 +5,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index + @group_links = @project.project_group_links + @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) if params[:search].present? users = @project.users.search(params[:search]).to_a @project_members = @project_members.where(user_id: users) - end - - @project_members = @project_members.order('access_level DESC') - - @group = @project.group - - if @group - @group_members = @group.group_members - @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) - - if params[:search].present? - users = @group.users.search(params[:search]).to_a - @group_members = @group_members.where(user_id: users) - end - @group_members = @group_members.order('access_level DESC') + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end + @project_members = @project_members.order(access_level: :desc).page(params[:page]) + @requesters = AccessRequestsFinder.new(@project).execute(current_user) @project_member = @project.project_members.new - @project_group_links = @project.project_group_links end def create @@ -43,6 +32,21 @@ class Projects::ProjectMembersController < Projects::ApplicationController current_user: current_user ) + if params[:group_ids].present? + group_ids = params[:group_ids].split(',') + groups = Group.where(id: group_ids) + + groups.each do |group| + next unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, + group_access: params[:access_level], + expires_at: params[:expires_at] + ) + end + end + redirect_to namespace_project_project_members_path(@project.namespace, @project) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 838ecc837e4..6a881b271d7 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController skip_before_action :authenticate_user! - before_action :user + before_action :user, except: [:exists] before_action :authorize_read_user!, only: [:show] def show @@ -85,6 +85,10 @@ class UsersController < ApplicationController render 'calendar_activities', layout: false end + def exists + render json: { exists: Namespace.where(path: params[:username].downcase).any? } + end + private def authorize_read_user! diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5dbf66173de..87475119b23 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,6 +1,7 @@ module Ci class Build < CommitStatus include TokenAuthenticatable + include AfterCommitQueue belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' @@ -75,25 +76,20 @@ module Ci state_machine :status do after_transition pending: :running do |build| - build.execute_hooks + build.run_after_commit do + BuildHooksWorker.perform_async(id) + end end after_transition any => [:success, :failed, :canceled] do |build| - build.update_coverage - build.execute_hooks + build.run_after_commit do + BuildFinishedWorker.perform_async(id) + end end after_transition any => [:success] do |build| - if build.environment.present? - service = CreateDeploymentService.new( - build.project, build.user, - environment: build.environment, - sha: build.sha, - ref: build.ref, - tag: build.tag, - options: build.options.to_h[:environment], - variables: build.variables) - service.execute(build) + build.run_after_commit do + BuildSuccessWorker.perform_async(id) end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 957f6755b2e..4fdb5fef4fb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -76,7 +76,11 @@ module Ci end after_transition do |pipeline, transition| - pipeline.execute_hooks unless transition.loopback? + next if transition.loopback? + + pipeline.run_after_commit do + PipelineHooksWorker.perform_async(id) + end end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index f63cc179b9e..3d9902d496e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -84,6 +84,10 @@ class Deployment < ActiveRecord::Base take end + def formatted_deployment_time + created_at.to_time.in_time_zone.to_s(:medium) + end + private def ref_path diff --git a/app/models/environment.rb b/app/models/environment.rb index f0f3ee23223..d970bc0a005 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -48,7 +48,22 @@ class Environment < ActiveRecord::Base self.name == "production" end + def first_deployment_for(commit) + ref = project.repository.ref_name_for_sha(ref_path, commit.sha) + + return nil unless ref + + deployment_id = ref.split('/').last + deployments.find(deployment_id) + end + def ref_path "refs/environments/#{Shellwords.shellescape(name)}" end + + def formatted_external_url + return nil unless external_url + + external_url.gsub(/\A.*?:\/\//, '') + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a743bf313ae..5ccfe11a2a2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -688,12 +688,15 @@ class MergeRequest < ActiveRecord::Base def environments return [] unless diff_head_commit - environments = source_project.environments_for( - source_branch, diff_head_commit) - environments += target_project.environments_for( - target_branch, diff_head_commit, with_tags: true) - - environments.uniq + @environments ||= + begin + environments = source_project.environments_for( + source_branch, diff_head_commit) + environments += target_project.environments_for( + target_branch, diff_head_commit, with_tags: true) + + environments.uniq + end end def state_human_name diff --git a/app/models/project.rb b/app/models/project.rb index 758927edd5c..db3088677d8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -490,7 +490,7 @@ class Project < ActiveRecord::Base end def import_url - if import_data && super + if import_data && super.present? import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials) import_url.full_url else @@ -829,11 +829,6 @@ class Project < ActiveRecord::Base end end - def update_merge_requests(oldrev, newrev, ref, user) - MergeRequests::RefreshService.new(self, user). - execute(oldrev, newrev, ref) - end - def valid_repo? repository.exists? rescue diff --git a/app/models/repository.rb b/app/models/repository.rb index 608c99eed46..72e473871fa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -719,6 +719,14 @@ class Repository end end + def ref_name_for_sha(ref_path, sha) + args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + + # Not found -> ["", 0] + # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] + Gitlab::Popen.popen(args, path_to_repo).first.split.last + end + def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 799ad3e1bd0..ff9a8310a8c 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -2,25 +2,35 @@ require_relative 'base_service' class CreateDeploymentService < BaseService def execute(deployable = nil) - environment = find_or_create_environment + return unless executable? - deployment = project.deployments.create( - environment: environment, + ActiveRecord::Base.transaction do + @deployable = deployable + @environment = prepare_environment + + deploy.tap do |deployment| + deployment.update_merge_request_metrics! + end + end + end + + private + + def executable? + project && name.present? + end + + def deploy + project.deployments.create( + environment: @environment, ref: params[:ref], tag: params[:tag], sha: params[:sha], user: current_user, - deployable: deployable - ) - - deployment.update_merge_request_metrics! - - deployment + deployable: @deployable) end - private - - def find_or_create_environment + def prepare_environment project.environments.find_or_create_by(name: expanded_name) do |environment| environment.external_url = expanded_url end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c499427605a..e8415862de5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -63,13 +63,12 @@ class GitPushService < BaseService protected def update_merge_requests - @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) + UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) EventCreateService.new.push(@project, current_user, build_push_data) - SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute + Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute ProjectCacheWorker.perform_async(@project.id) end @@ -148,16 +147,6 @@ class GitPushService < BaseService push_commits) end - def build_push_data_system_hook - @push_data_system ||= Gitlab::DataBuilder::Push.build( - @project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - []) - end - def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml index 6c51639b840..acbe17036f7 100644 --- a/app/views/admin/appearances/preview.html.haml +++ b/app/views/admin/appearances/preview.html.haml @@ -1,9 +1,12 @@ -- page_title "Preview | Appearance" += render 'devise/shared/tab_single', tab_title: 'Sign in preview' .login-box - .login-heading - %h3 Existing user? Sign in - %form - = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" - = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" - = button_tag "Sign in", class: "btn-create btn" + %form.show-gl-field-errors + .form-group + = label_tag :login + = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.' + .form-group + = label_tag :password + = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.' + .form-group + = button_tag "Sign in", class: "btn-create btn" diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index a5e82e55cc1..10fea1996aa 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -71,7 +71,7 @@ .col-md-6 %h4 Recent builds served by this Runner - %table.table.builds.runner-builds + %table.table.ci-table.runner-builds %thead %tr %th Build diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml index 970ba147111..5d25dd398d6 100644 --- a/app/views/devise/confirmations/new.html.haml +++ b/app/views/devise/confirmations/new.html.haml @@ -1,14 +1,14 @@ += render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions' .login-box - .login-heading - %h3 Resend confirmation instructions .login-body - = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, placeholder: 'Email', class: "form-control", required: true + .form-group + = f.label :email + = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.' .clearfix - = f.submit "Resend confirmation instructions", class: 'btn btn-success' + = f.submit "Resend", class: 'btn btn-success' .clearfix.prepend-top-20 = render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 56048e99c17..b518fae7c95 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -1,19 +1,21 @@ += render 'devise/shared/tab_single', tab_title:'Change your password' .login-box - .login-heading - %h3 Change your password .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! = f.hidden_field :reset_password_token - %div - = f.password_field :password, class: "form-control top", placeholder: "New password", required: true - %div - = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true + .form-group + = f.label 'New password', for: :password + = f.password_field :password, class: "form-control top", required: true, title: 'This field is required' + .form-group + = f.label 'Confirm new password', for: :password_confirmation + = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true .clearfix = f.submit "Change your password", class: "btn btn-primary" .clearfix.prepend-top-20 %p - = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) - = render 'devise/shared/sign_in_link' + %span.light Didn't receive a confirmation email? + = link_to "Request a new one", new_confirmation_path(resource_name) += render 'devise/shared/sign_in_link' diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 535e85869e5..1fcfd06419a 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -1,12 +1,12 @@ += render 'devise/shared/tab_single', tab_title: 'Reset Password' .login-box - .login-heading - %h3 Reset password .login-body - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true + .form-group + = f.label :email + = f.email_field :email, class: "form-control", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.' .clearfix = f.submit "Reset password", class: "btn-primary btn" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 9f5520603cd..a96b579c593 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,6 +1,10 @@ -= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" - = f.password_field :password, class: "form-control bottom", placeholder: "Password" += form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-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." + %div.form-group + = f.label :password + = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} @@ -8,5 +12,5 @@ %span Remember me .pull-right = link_to "Forgot your password?", new_password_path(resource_name) - %div + %div.submit-container = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index b7d3acac2b1..e82a08cdb0c 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,6 +1,10 @@ -= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"} - = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} += form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do + .form-group + = label_tag 'Username or email', for: :username + = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } + .form-group + = label_tag :password + = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "remember_me"} diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 2ef383960f4..b26efbb4535 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,6 +1,10 @@ -= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} - = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} += form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do + .form-group + = label_tag "#{server['label']} Login", for: :username + = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } + .form-group + = label_tag :password + = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "remember_me"} diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 28194506acc..fa8e7979461 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,19 +1,22 @@ - page_title "Sign in" %div - - if signin_enabled? || ldap_enabled? || crowd_enabled? - = render 'devise/shared/signin_box' + - if form_based_providers.any? + = render 'devise/shared/tabs_ldap' + - else + = render 'devise/shared/tabs_normal' + .tab-content + - if signin_enabled? || ldap_enabled? || crowd_enabled? + = render 'devise/shared/signin_box' - -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? - .clearfix.prepend-top-20 - = render 'devise/shared/omniauth_box' - - -# Signup only makes sense if you can also sign-in - - if signin_enabled? && signup_enabled? - .prepend-top-20 + -# Signup only makes sense if you can also sign-in + - if signin_enabled? && signup_enabled? = render 'devise/shared/signup_box' -# Show a message if none of the mechanisms above are enabled - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?) %div No authentication methods configured. + + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? + .clearfix + = render 'devise/shared/omniauth_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index e623f7cff88..0e865b807c1 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -3,20 +3,19 @@ = page_specific_javascript_tag('u2f.js') %div + = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' .login-box - .login-heading - %h3 Two-Factor Authentication .login-body - if @user.two_factor_otp_enabled? - %h5 Authenticate via Two-Factor App - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. - .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + .form-group + = f.label 'Two-Factor Authentication code', name: :otp_attempt + = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" - if @user.two_factor_u2f_enabled? - %hr = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name } diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 2e7da2747d0..8908b64cdac 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,8 +1,9 @@ -%p - %span.light - Sign in with - - providers = enabled_button_based_providers - - providers.each do |provider| +%div.omniauth-container + %p %span.light - - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + Sign in with + - providers = enabled_button_based_providers + - providers.each do |provider| + %span.light + - has_icon = provider_has_icon?(provider) + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_sign_in_link.html.haml b/app/views/devise/shared/_sign_in_link.html.haml index fafc4b82f53..289bf40f3de 100644 --- a/app/views/devise/shared/_sign_in_link.html.haml +++ b/app/views/devise/shared/_sign_in_link.html.haml @@ -1,5 +1,4 @@ %p %span.light Already have login and password? - %strong = link_to "Sign in", new_session_path(resource_name) diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 2c15e2c4891..810dd5ab687 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,32 +1,15 @@ -.login-box - - if signup_enabled? - .login-heading - %h3 Existing user? Sign in - - else - .login-heading - %h3 Sign in +#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' } .login-body - if form_based_providers.any? - %ul.nav-links - - if crowd_enabled? - %li.active - = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' - - @ldap_servers.each_with_index do |server, i| - %li{class: (:active if i.zero? && !crowd_enabled?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' - - if signin_enabled? - %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' - .tab-content - - if crowd_enabled? - %div.tab-pane.active{id: "tab-crowd"} - = render 'devise/sessions/new_crowd' - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} - = render 'devise/sessions/new_ldap', server: server - - if signin_enabled? - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' + - if crowd_enabled? + %div.tab-pane.active{id: "tab-crowd"} + = render 'devise/sessions/new_crowd' + - @ldap_servers.each_with_index do |server, i| + %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + %div#tab-signin.tab-pane + = render 'devise/sessions/new_base' - elsif signin_enabled? = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 905a8dbcd84..d0bbcf3115e 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,29 +1,30 @@ -.login-box - - if signin_enabled? - .login-heading - %h3 New user? Create an account - - else - .login-heading - %h3 Create an account +#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } .login-body - = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f| + = 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| .devise-errors = devise_error_messages! - %div - = f.text_field :name, class: "form-control top", placeholder: "Name", required: true - %div - = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true - %div - = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true + %div.form-group + = f.label :name + = 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.' + %p.validation-error.hide Username is already taken. + %p.validation-success.hide Username is available. + %p.validation-pending.hide Checking username availability... + %div.form-group + = f.label :email + = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters" + = f.label :password + = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." + %p.gl-field-hint Minimum length is #{@minimum_password_length} characters %div - if current_application_settings.recaptcha_enabled = recaptcha_tags %div - = f.submit "Sign up", class: "btn-create btn" - -.clearfix.prepend-top-20 + = f.submit "Register", class: "btn-register btn" +.clearfix.submit-container %p %span.light Didn't receive a confirmation email? = succeed '.' do diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml new file mode 100644 index 00000000000..f943d25e41a --- /dev/null +++ b/app/views/devise/shared/_tab_single.html.haml @@ -0,0 +1,3 @@ +%ul.nav-links.nav-tabs.new-session-tabs.single-tab + %li.active + %a= tab_title diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml new file mode 100644 index 00000000000..e276e91433a --- /dev/null +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -0,0 +1,10 @@ +%ul.new-session-tabs.nav-links.nav-tabs + - if crowd_enabled? + %li.active + = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' + - @ldap_servers.each_with_index do |server, i| + %li{class: (:active if i.zero? && !crowd_enabled?)} + = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + - if signin_enabled? + %li + = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml new file mode 100644 index 00000000000..79b1d447a92 --- /dev/null +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -0,0 +1,5 @@ +%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} + %li.active{ role: 'presentation' } + %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in + %li{ role: 'presentation'} + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml index 49c087c0646..49b2f77111f 100644 --- a/app/views/devise/unlocks/new.html.haml +++ b/app/views/devise/unlocks/new.html.haml @@ -1,12 +1,12 @@ += render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions' .login-box - .login-heading - %h3 Resend unlock email .login-body - = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f| .devise-errors = devise_error_messages! - .clearfix.append-bottom-20 - = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' + .form-group.append-bottom-20 + = f.label :email + = f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.' .clearfix = f.submit 'Resend unlock instructions', class: 'btn btn-success' diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 2fb3190ab11..b185b81db7f 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,27 +1,22 @@ -= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) - .help-block += form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f| + .row + .col-md-4.col-lg-6 + = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) + .help-block.append-bottom-10 Search for users by name, username, or email, or invite new ones using their email address. - .form-group - = f.label :access_level, "Group Access", class: 'control-label' - .col-sm-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2" - .help-block - Read more about role permissions - %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .col-md-3.col-lg-2 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions - .form-group - = f.label :expires_at, 'Access expiration date', class: 'control-label' - .col-sm-10 + .col-md-3.col-lg-2 .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - .help-block + .help-block.append-bottom-10 On this date, the user(s) will automatically lose access to this group and all of its projects. - .form-actions - = f.submit 'Add users to group', class: "btn btn-create" + .col-md-2 + = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f789796e942..ebf9aca7700 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,35 +1,31 @@ - page_title "Members" -.group-members-page.prepend-top-default +.project-members-page.prepend-top-default + %h4 + Members + %hr - if can?(current_user, :admin_group_member, @group) - .panel.panel-default - .panel-heading - Add new user to group - .panel-body - %p.light - Members of group have access to all group projects. - .new-group-member-holder - = render "new_group_member" + .project-members-new.append-bottom-default + %p.clearfix + Add new user to + %strong= @group.name + = render "new_group_member" = render 'shared/members/requests', membership_source: @group, requesters: @requesters + .append-bottom-default.clearfix + %h5.member.existing-title + Existing users + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") .panel.panel-default .panel-heading + Users with access to %strong #{@group.name} - group members %span.badge= @members.total_count - .controls - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn', title: 'Search' do - = icon("search") %ul.content-list = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' - -:javascript - $('form.member-search-form').on('submit', function(event) { - event.preventDefault(); - Turbolinks.visit(this.action + '?' + $(this).serialize()); - }); diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index 3be7ed8432c..de8f53b6b52 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,3 +1,3 @@ :plain - $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); - new gl.MemberExpirationDate(); + var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); + $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 67ff4b272b9..e138ebab018 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,7 +1,8 @@ - project = @target_project || @project - noteable_type = @noteable.class if @noteable.present? -:javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" - GitLab.GfmAutoComplete.cachedData = undefined; - GitLab.GfmAutoComplete.setup(); +- if project + :javascript + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" + GitLab.GfmAutoComplete.cachedData = undefined; + GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 15a94ac23c5..6c2285fa2b6 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -11,3 +11,4 @@ = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body + = render "layouts/init_auto_complete" if @gfm_form diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index a9a384bd5f3..6922f1e153f 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,36 +1,37 @@ !!! 5 -%html{ lang: "en"} +%html{ lang: "en", class: "devise-layout-html"} = render "layouts/head" - %body.ui_charcoal.login-page.application.navless - = Gon::Base.render_data - = render "layouts/header/empty" - = render "layouts/broadcast" - .container.navless-container - .content - = render "layouts/flash" - .row - .col-sm-5.pull-right - = yield - .col-sm-7.brand-holder.pull-left - %h1 - = brand_title - - if brand_item - = brand_image - = brand_text - - else - %h3 Open source software to collaborate on code + %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }} + .page-wrap + = Gon::Base.render_data + = render "layouts/header/empty" + = render "layouts/broadcast" + .container.navless-container + .content + = render "layouts/flash" + .row + .col-sm-5.pull-right.new-session-forms-container + = yield + .col-sm-7.brand-holder.pull-left + %h1 + = brand_title + - if brand_item + = brand_image + = brand_text + - else + %h3 Open source software to collaborate on code - %p - Manage git repositories with fine grained access controls that keep your code secure. - Perform code reviews and enhance collaboration with merge requests. - Each project can also have an issue tracker and a wiki. + %p + Manage Git repositories with fine-grained access controls that keep your code secure. + Perform code reviews and enhance collaboration with merge requests. + Each project can also have an issue tracker and a wiki. - if current_application_settings.sign_in_text.present? = markdown_field(current_application_settings, :sign_in_text) - %hr - .container - .footer-links - = link_to "Explore", explore_root_path - = link_to "Help", help_path - = link_to "About GitLab", "https://about.gitlab.com/" + %hr.footer-fixed + .container.footer-container + .footer-links + = link_to "Explore", explore_root_path + = link_to "Help", help_path + = link_to "About GitLab", "https://about.gitlab.com/" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index c80f22457b4..e2e974ba072 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -86,11 +86,11 @@ = f.label :username, "Path", class: "label-light" .input-group .input-group-addon - = "#{root_url}u/" + = root_url = f.text_field :username, required: true, class: 'form-control' .help-block Current path: - = "#{root_url}u/#{current_user.username}" + = "#{root_url}#{current_user.username}" .prepend-top-default = f.button class: "btn btn-warning", type: "submit" do = icon "spinner spin", class: "hidden loading-username" diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index cb97181b9e1..0c8241053e7 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,3 +1,4 @@ +- @gfm_form = true - supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' @@ -7,6 +8,3 @@ = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') - -- content_for :scripts_body do - = render "layouts/init_auto_complete" if current_user && (@target_project || @project) diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 5a98e258b22..dfb96305f48 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,45 +1,48 @@ +- @no_container = true - page_title "Blame", @blob.path, @ref += render "projects/commits/head" -%h3.page-title Blame view +%div{ class: container_class } + %h3.page-title Blame view -#blob-content-holder.tree-holder - .file-holder - .file-title - = blob_icon @blob.mode, @blob.name - %strong - = @path - %small= number_to_human_size @blob.size - .file-actions - = render "projects/blob/actions" - .table-responsive.file-content.blame.code.js-syntax-highlight - %table - - current_line = 1 - - @blame_groups.each do |blame_group| - %tr - %td.blame-commit - .commit - - commit = blame_group[:commit] - = author_avatar(commit, size: 36) - .commit-row-title - %strong - = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" - .pull-right - = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" - - .light - = commit_author_link(commit, avatar: false) - authored - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} - %td.line-numbers - - line_count = blame_group[:lines].count - - (current_line...(current_line + line_count)).each do |i| - %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} - = icon("link") - = i - \ - - current_line += line_count - %td.lines - %pre.code.highlight - %code - - blame_group[:lines].each do |line| - #{line} + #blob-content-holder.tree-holder + .file-holder + .file-title + = blob_icon @blob.mode, @blob.name + %strong + = @path + %small= number_to_human_size @blob.size + .file-actions + = render "projects/blob/actions" + .table-responsive.file-content.blame.code.js-syntax-highlight + %table + - current_line = 1 + - @blame_groups.each do |blame_group| + %tr + %td.blame-commit + .commit + - commit = blame_group[:commit] + = author_avatar(commit, size: 36) + .commit-row-title + %strong + = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" + .pull-right + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" + + .light + = commit_author_link(commit, avatar: false) + authored + #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + %td.line-numbers + - line_count = blame_group[:lines].count + - (current_line...(current_line + line_count)).each do |i| + %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} + = icon("link") + = i + \ + - current_line += line_count + %td.lines + %pre.code.highlight + %code + - blame_group[:lines].each do |line| + #{line} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 680e95ac6b5..2a0352a71b7 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,28 +1,31 @@ +- @no_container = true - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') += render "projects/commits/head" -- if @conflict - .alert.alert-danger - Someone edited the file the same time you did. Please check out - = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" - and make sure your changes will not unintentionally remove theirs. +%div{ class: container_class } + - if @conflict + .alert.alert-danger + Someone edited the file the same time you did. Please check out + = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" + and make sure your changes will not unintentionally remove theirs. -.file-editor - %ul.nav-links.no-bottom.js-edit-mode - %li.active - = link_to '#editor' do - Edit File + .file-editor + %ul.nav-links.no-bottom.js-edit-mode + %li.active + = link_to '#editor' do + Edit File - %li - = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - = editing_preview_title(@blob.name) + %li + = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do + = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do - = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data - = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" - = hidden_field_tag 'last_commit_sha', @last_commit_sha - = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do + = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data + = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" + = hidden_field_tag 'last_commit_sha', @last_commit_sha + = hidden_field_tag 'content', '', id: "file-content" + = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index f3747ba2a21..36294c89fa8 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -5,7 +5,7 @@ .nothing-here-block No builds to show - else .table-holder - %table.table.builds + %table.table.ci-table.builds-page %thead %tr %th Status diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index e4d41288aa6..b5e8b0bf6eb 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,56 +1,59 @@ +- @no_container = true - page_title "#{@build.name} (##{@build.id})", "Builds" - trace_with_state = @build.trace_with_state - header_title project_title(@project, "Builds", project_builds_path(@project)) += render "projects/pipelines/head", build_subnav: true -.build-page - = render "header" +%div{ class: container_class } + .build-page + = render "header" - - if @build.stuck? - - unless @build.any_runners_online? - .bs-callout.bs-callout-warning - %p - - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have any runners online assigned to it. - - elsif @build.tags.any? - This build is stuck, because you don't have any active runners online with any of these tags assigned to them: - - @build.tags.each do |tag| - %span.label.label-primary - = tag - - else - This build is stuck, because you don't have any active runners that can run this build. + - if @build.stuck? + - unless @build.any_runners_online? + .bs-callout.bs-callout-warning + %p + - if no_runners_for_project?(@build.project) + This build is stuck, because the project doesn't have any runners online assigned to it. + - elsif @build.tags.any? + This build is stuck, because you don't have any active runners online with any of these tags assigned to them: + - @build.tags.each do |tag| + %span.label.label-primary + = tag + - else + This build is stuck, because you don't have any active runners that can run this build. - %br - Go to - = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do - Runners page + %br + Go to + = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do + Runners page - .prepend-top-default - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - - if @build.erased? - .erased.alert.alert-warning - - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - - else - #js-build-scroll.scroll-controls - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down - %pre.build-trace#build-trace - %code.bash.js-build-output - = icon("refresh spin", class: "js-build-refresh") + .prepend-top-default + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + - if @build.erased? + .erased.alert.alert-warning + - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by + Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - else + #js-build-scroll.scroll-controls + = link_to '#build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + %pre.build-trace#build-trace + %code.bash.js-build-output + = icon("refresh spin", class: "js-build-refresh") - #down-build-trace + #down-build-trace -= render "sidebar" + = render "sidebar" -:javascript - new Build({ - page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}", - build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", - build_status: "#{@build.status}", - build_stage: "#{@build.stage}", - state1: "#{trace_with_state[:state]}" - }) + :javascript + new Build({ + page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}", + build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", + build_status: "#{@build.status}", + build_stage: "#{@build.stage}", + state1: "#{trace_with_state[:state]}" + }) diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 288c06d9b67..d6916fb7f1a 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -56,7 +56,7 @@ \.gitlab-ci.yml not found in this commit .table-holder.pipeline-holder - %table.table.builds.pipeline + %table.table.ci-table.pipeline %thead %tr %th Status diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 998812793a2..640651e93f5 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -4,7 +4,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.builds + %table.table.ci-table %tbody %th Status %th Pipeline diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 2f051fb90e0..f9d7eac3542 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,7 +1,10 @@ +- @no_container = true - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/head" -.prepend-top-default - = render "commit_box" +%div{ class: container_class } + .prepend-top-default + = render "commit_box" -= render "ci_menu" -= render "builds" + = render "ci_menu" + = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index ed44d86a687..cebf58d63df 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,14 +1,17 @@ +- @no_container = true - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description += render "projects/commits/head" -.prepend-top-default - = render "commit_box" -- if @commit.status - = render "ci_menu" -- else - %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs -= render "projects/notes/notes_with_form" -- if can_collaborate_with_project? - - %w(revert cherry-pick).each do |type| - = render "projects/commit/change", type: type, commit: @commit, title: @commit.title +%div{ class: container_class } + .prepend-top-default + = render "commit_box" + - if @commit.status + = render "ci_menu" + - else + %div.block-connector + = render "projects/diffs/diffs", diffs: @diffs + = render "projects/notes/notes_with_form" + - if can_collaborate_with_project? + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml index 6d1bdb9320f..3871165763c 100644 --- a/app/views/projects/environments/edit.html.haml +++ b/app/views/projects/environments/edit.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "Edit", @environment.name, "Environments" += render "projects/pipelines/head" -%h3.page-title - Edit environment -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + Edit environment + %hr + = render 'form' diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index ab801409722..721ba156334 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -24,7 +24,7 @@ New environment - else .table-holder - %table.table.builds.environments + %table.table.ci-table.environments %tbody %th Environment %th Last Deployment diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index e51667ade2d..24638c77cbb 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title 'New Environment' += render "projects/pipelines/head" -%h3.page-title - New environment -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + New environment + %hr + = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7a8d196cf4e..90c59223a35 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -24,7 +24,7 @@ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder - %table.table.builds.environments + %table.table.ci-table.environments %thead %tr %th ID diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml new file mode 100644 index 00000000000..af9a5b19060 --- /dev/null +++ b/app/views/projects/group_links/update.js.haml @@ -0,0 +1,3 @@ +:plain + var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); + $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 6901ba13ab7..52b187e7e58 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "Edit", @label.name, "Labels" += render "projects/issues/head" -%h3.page-title - Edit Label -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + Edit Label + %hr + = render 'form' diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 49ddf901619..a1bb66cfb6c 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,6 +1,9 @@ +- @no_container = true - page_title "New Label" += render "projects/issues/head" -%h3.page-title - New Label -%hr -= render 'form' +%div{ class: container_class } + %h3.page-title + New Label + %hr + = render 'form' diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 47dd51639b5..662463bc72b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -47,7 +47,7 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits_count.nonzero? - %ul.merge-request-tabs.nav-links.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 5b7f83c344f..a82c846baa7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -44,17 +44,5 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.sort_by(&:name).each do |environment| - - if can?(current_user, :read_environment, environment) - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span - Deployed to - = succeed '.' do - = link_to environment.name, environment_path(environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')} - = icon('external-link', right: true) +.js-success-icon.hidden + = ci_icon_for_status('success') diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index ea618263a4a..608fdf1c5f5 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -12,6 +12,7 @@ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", ci_message: { @@ -33,4 +34,4 @@ merge_request_widget.clearEventListeners(); } - merge_request_widget = new MergeRequestWidget(opts); + merge_request_widget = new window.gl.MergeRequestWidget(opts); diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index be682226ab6..11f41e75e63 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,8 +1,12 @@ +- @no_container = true - page_title "Edit", @milestone.title, "Milestones" += render "projects/issues/head" -%h3.page-title - Edit Milestone ##{@milestone.iid} +%div{ class: container_class } -%hr + %h3.page-title + Edit Milestone ##{@milestone.iid} -= render "form" + %hr + + = render "form" diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 7f372b41698..cda093ade81 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,8 +1,11 @@ +- @no_container = true - page_title "New Milestone" += render "projects/issues/head" -%h3.page-title - New Milestone +%div{ class: container_class } + %h3.page-title + New Milestone -%hr + %hr -= render "form" + = render "form" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index e62f810a521..c83818e9199 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,49 +1,52 @@ +- @no_container = true - page_title @milestone.title, "Milestones" - page_description @milestone.description += render "projects/issues/head" -.detail-page-header - .status-box{ class: status_box_class(@milestone) } - - if @milestone.closed? - Closed - - elsif @milestone.expired? - Past due - - else - Open - %span.identifier - Milestone ##{@milestone.iid} - - if @milestone.expires_at - %span.creator - · - = @milestone.expires_at - .pull-right - - if can?(current_user, :admin_milestone, @project) - - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" +%div{ class: container_class } + .detail-page-header + .status-box{ class: status_box_class(@milestone) } + - if @milestone.closed? + Closed + - elsif @milestone.expired? + Past due - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" + Open + %span.identifier + Milestone ##{@milestone.iid} + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at + .pull-right + - if can?(current_user, :admin_milestone, @project) + - if @milestone.active? + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" + - else + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do - Edit + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do + Edit - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do - Delete + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do + Delete -.detail-page-description.milestone-detail - %h2.title - = markdown_field(@milestone, :title) - %div - - if @milestone.description.present? - .description - .wiki - = preserve do - = markdown_field(@milestone, :description) + .detail-page-description.milestone-detail + %h2.title + = markdown_field(@milestone, :title) + %div + - if @milestone.description.present? + .description + .wiki + = preserve do + = markdown_field(@milestone, :description) -- if @milestone.total_items_count(current_user).zero? - .alert.alert-success.prepend-top-default - %span Assign some issues to this milestone. -- elsif @milestone.complete?(current_user) && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close this milestone now. + - if @milestone.total_items_count(current_user).zero? + .alert.alert-success.prepend-top-default + %span Assign some issues to this milestone. + - elsif @milestone.complete?(current_user) && @milestone.active? + .alert.alert-success.prepend-top-default + %span All issues for this milestone are closed. You may close this milestone now. -= render 'shared/milestones/summary', milestone: @milestone, project: @project -= render 'shared/milestones/tabs', milestone: @milestone + = render 'shared/milestones/summary', milestone: @milestone, project: @project + = render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 8352eba7446..00b62a595ff 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -14,7 +14,7 @@ .disabled-comment.text-center .disabled-comment-text.inline Please - = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "register", new_session_path(:user, redirect_to_referer: 'yes') or = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes') to post a comment diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 7d421c0e740..b10dd47709f 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,7 +1,7 @@ = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs + .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } %ul{ class: (container_class) } - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 2d1df095bfa..9eeef5f57b4 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -43,7 +43,7 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.builds + %table.table.ci-table %thead %th.col-xs-1.col-sm-1 Status %th.col-xs-2.col-sm-4 Pipeline diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 75943c64276..688535ad764 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -1,8 +1,11 @@ +- @no_container = true - page_title "Pipeline" += render "projects/pipelines/head" -.prepend-top-default - - if @commit - = render "projects/pipelines/info" - %div.block-connector +%div{ class: container_class } + .prepend-top-default + - if @commit + = render "projects/pipelines/info" + %div.block-connector -= render "projects/commit/pipeline", pipeline: @pipeline + = render "projects/commit/pipeline", pipeline: @pipeline diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index e783d8c72c5..9738f369a35 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -1,7 +1,7 @@ .panel.panel-default .panel-heading + Group members with access to %strong #{@group.name} - group members %span.badge= members.size - if can?(current_user, :admin_group_member, @group) .controls diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml new file mode 100644 index 00000000000..d7f5fa96527 --- /dev/null +++ b/app/views/projects/project_members/_groups.html.haml @@ -0,0 +1,7 @@ +.panel.panel-default.project-members-groups + .panel-heading + Groups with access to + %strong #{@project.name} + %span.badge= group_links.size + %ul.content-list + = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index fa8cbf71733..79dcd7a6ee9 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,27 +1,22 @@ -= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true) - .help-block += form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| + .row + .col-md-4.col-lg-6 + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true) + .help-block.append-bottom-10 Search for users by name, username, or email, or invite new ones using their email address. - .form-group - = f.label :access_level, "Project Access", class: 'control-label' - .col-sm-10 - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2" - .help-block - Read more about role permissions - %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .col-md-3.col-lg-2 + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions - .form-group - = f.label :expires_at, 'Access expiration date', class: 'control-label' - .col-sm-10 + .col-md-3.col-lg-2 .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - .help-block + .help-block.append-bottom-10 On this date, the user(s) will automatically lose access to this project. - .form-actions - = f.submit 'Add users to project', class: "btn btn-create" + .col-md-2 + = f.submit "Add to project", class: "btn btn-create btn-block" diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index b0bfdd235f7..c1e894d8f40 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,19 +1,7 @@ .panel.panel-default .panel-heading + Users with access to %strong #{@project.name} - project members - %span.badge= members.size - .controls - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } - = button_tag class: 'btn', title: 'Search' do - = icon("search") + %span.badge= @project_members.total_count %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member - -:javascript - $('form.member-search-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '?' + $(this).serialize()); - }); diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9d063b3081f..bdeb704b6da 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,24 +1,28 @@ - page_title "Members" -.project-members-page.js-project-members-page.prepend-top-default +.project-members-page.prepend-top-default + %h4.project-members-title.clearfix + Members + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project" - if can?(current_user, :admin_project_member, @project) - .panel.panel-default - .panel-heading - Add new user to project - .controls - = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - .panel-body - %p.light - Users with access to this project are listed below. - = render "new_project_member" + .project-members-new.append-bottom-default + %p.clearfix + Add new user to + %strong= @project.name + = render "new_project_member" - = render 'shared/members/requests', membership_source: @project, requesters: @requesters + = render 'shared/members/requests', membership_source: @project, requesters: @requesters - = render 'team', members: @project_members - - - if @group - = render "group_members", members: @group_members + .append-bottom-default.clearfix + %h5.member.existing-title + Existing users and groups + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + - if @group_links.any? + = render 'groups', group_links: @group_links - - if @project_group_links.any? && @project.allowed_to_share_with_group? - = render "shared_group_members" + = render 'team', members: @project_members + = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 37e55dc72a3..91927181efb 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,3 @@ :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); - new gl.MemberExpirationDate(); + var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); + $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c3f4e10c954..a7944a60130 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -23,6 +23,8 @@ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do %ul.dropdown-footer-list %li + %a.no-template + No template %a.reset-template Reset template %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } @@ -85,20 +87,20 @@ .issuable-form-select-holder - if issuable.assignee_id = f.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } }) .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input" + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' } + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 6d307611640..22b5a6aa11b 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -8,6 +8,7 @@ - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) - selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options @@ -23,7 +24,7 @@ = multi_label_name(selected, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index ab3cc33d18f..f27a9002ec2 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -2,9 +2,10 @@ - extra_class = extra_class || '' - show_menu_above = show_menu_above || false - selected_text = selected.try(: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) -= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", += 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 %ul.dropdown-footer-list diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml new file mode 100644 index 00000000000..1c0346bbc78 --- /dev/null +++ b/app/views/shared/members/_group.html.haml @@ -0,0 +1,29 @@ +- group_link = local_assigns[:group_link] +- group = group_link.group +- can_admin_member = can?(current_user, :admin_project_member, @project) +%li.member.group_member{ id: "group_member_#{group_link.id}" } + %span{ class: "list-item-name" } + = image_tag group_icon(group), class: "avatar s40", alt: '' + %strong + = link_to group.name, group_path(group) + .cgray + Joined #{time_ago_with_tooltip(group.created_at)} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} + .controls.member-controls + = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do + = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member + .prepend-left-5.clearable-input.member-form-control + = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member + %i.clear-icon.js-clear-input + - if can_admin_member + = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), + remote: true, + method: :delete, + data: { confirm: "Are you sure you want to remove #{group.name}?" }, + class: 'btn btn-remove prepend-left-10' do + %span.visible-xs-block + Delete + = icon('trash', class: 'hidden-xs') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 5f20e4bd42a..432047a1c4e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,59 +1,29 @@ - show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) -- user = member.user +- user = local_assigns.fetch(:user, member.user) +- source = member.source +- can_admin_member = can?(current_user, action_member_permission(:update, member), member) -%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) } - - if show_roles - .controls - %strong.control-text= member.human_access - - if show_controls - - if !user && can?(current_user, action_member_permission(:admin, member), member.source) - = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), - method: :post, - class: 'btn' - - - if can?(current_user, action_member_permission(:update, member), member) - = button_tag icon('pencil'), - type: 'button', - class: 'btn inline js-toggle-button', - title: 'Edit' - - - if member.request? - = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), - method: :post, - class: 'btn btn-success', - title: 'Grant access' - - - if can?(current_user, action_member_permission(:destroy, member), member) - - if current_user == user - = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), - method: :delete, - data: { confirm: leave_confirmation_message(member.source) }, - class: 'btn btn-remove' - - else - = link_to icon('trash'), member, - remote: true, - method: :delete, - data: { confirm: remove_member_message(member) }, - class: 'btn btn-remove', - title: remove_member_title(member) - - - %span{ class: ("list-item-name" if show_controls) } +%li.member{ class: dom_class(member), id: dom_id(member) } + %span.list-item-name - if user = image_tag avatar_icon(user, 40), class: "avatar s40", alt: '' %strong = link_to user.name, user_path(user) - %span.cgray= user.username + %span.cgray= user.to_reference - if user == current_user - %span.label.label-success It's you + %span.label.label-success.prepend-left-5 It's you - if user.blocked? %label.label.label-danger %strong Blocked - .cgray + - if source.instance_of?(Group) && !@group + = link_to source, class: "member-group-link prepend-left-5" do + = "· #{source.name}" + + .hidden-xs.cgray - if member.request? Requested = time_ago_with_tooltip(member.requested_at) @@ -73,20 +43,44 @@ by = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_roles - .edit-member.hide.js-toggle-content - %br - = form_for member, remote: true, html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' - .col-sm-10 - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" - .form-group - = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label' - .col-sm-10 - .clearable-input - = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}" + .controls.member-controls + - if show_controls + - if user != current_user + = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member + .prepend-left-5.clearable-input.member-form-control + = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input - .prepend-top-10 - = f.submit 'Save', class: 'btn btn-save btn-sm' + - else + %span.member-access-text= member.human_access + + - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) + = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), + method: :post, + class: 'btn btn-default prepend-left-10' + + - elsif member.request? && can_admin_member + = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), + method: :post, + class: 'btn btn-success prepend-left-10', + title: 'Grant access' + + - if can?(current_user, action_member_permission(:destroy, member), member) + - if current_user == user + = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(member.source) }, + class: 'btn btn-remove prepend-left-10' + - else + = link_to member, + remote: true, + method: :delete, + data: { confirm: remove_member_message(member) }, + class: 'btn btn-remove prepend-left-10', + title: remove_member_title(member) do + %span.visible-xs-block + Delete + = icon('trash', class: 'hidden-xs') + - else + %span.member-access-text= member.human_access diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 40b39e850b0..10050adfda5 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,8 +1,8 @@ - if requesters.any? .panel.panel-default .panel-heading + Users requesting access to %strong= membership_source.name - access requests %span.badge= requesters.size %ul.content-list = render partial: 'shared/members/member', collection: requesters, as: :member diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 9657101ace5..232ca26c1af 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -6,7 +6,7 @@ %script#js-authenticate-u2f-setup{ type: "text/template" } %div %p Insert your security key (if you haven't already), and press the button below. - %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device + %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device %script#js-authenticate-u2f-in-progress{ type: "text/template" } %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb new file mode 100644 index 00000000000..0680645a8db --- /dev/null +++ b/app/workers/build_coverage_worker.rb @@ -0,0 +1,9 @@ +class BuildCoverageWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id) + .try(:update_coverage) + end +end diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb new file mode 100644 index 00000000000..e7286b77ac5 --- /dev/null +++ b/app/workers/build_finished_worker.rb @@ -0,0 +1,10 @@ +class BuildFinishedWorker + include Sidekiq::Worker + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + BuildCoverageWorker.new.perform(build.id) + BuildHooksWorker.new.perform(build.id) + end + end +end diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb new file mode 100644 index 00000000000..e22ececb3fd --- /dev/null +++ b/app/workers/build_hooks_worker.rb @@ -0,0 +1,9 @@ +class BuildHooksWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id) + .try(:execute_hooks) + end +end diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb new file mode 100644 index 00000000000..500d357ce31 --- /dev/null +++ b/app/workers/build_success_worker.rb @@ -0,0 +1,27 @@ +class BuildSuccessWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + create_deployment(build) + end + end + + private + + def create_deployment(build) + return if build.environment.blank? + + service = CreateDeploymentService.new( + build.project, build.user, + environment: build.environment, + sha: build.sha, + ref: build.ref, + tag: build.tag, + options: build.options.to_h[:environment], + variables: build.variables) + + service.execute(build) + end +end diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb new file mode 100644 index 00000000000..ab5e9f6daad --- /dev/null +++ b/app/workers/pipeline_hooks_worker.rb @@ -0,0 +1,9 @@ +class PipelineHooksWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id) + .try(:execute_hooks) + end +end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb new file mode 100644 index 00000000000..03f0528cdae --- /dev/null +++ b/app/workers/update_merge_requests_worker.rb @@ -0,0 +1,16 @@ +class UpdateMergeRequestsWorker + include Sidekiq::Worker + + def perform(project_id, user_id, oldrev, newrev, ref) + project = Project.find_by(id: project_id) + return unless project + + user = User.find_by(id: user_id) + return unless user + + MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) + + push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, []) + SystemHooksService.new.execute_hooks(push_data, :push_hooks) + end +end diff --git a/config/routes.rb b/config/routes.rb index 83c3a42c19f..659ea51bc75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -88,4 +88,6 @@ Rails.application.routes.draw do get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } root to: "root#index" + + get '*unmatched_route', to: 'application#not_found' end diff --git a/config/routes/group.rb b/config/routes/group.rb index 47a8a0a53d4..06b464d79c8 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,8 +1,14 @@ require 'constraints/group_url_constrainer' constraints(GroupUrlConstrainer.new) do - scope(path: ':id', as: :group, controller: :groups) do + scope(path: ':id', + as: :group, + constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, + controller: :groups) do get '/', action: :show + patch '/', action: :update + put '/', action: :update + delete '/', action: :destroy end end diff --git a/config/routes/project.rb b/config/routes/project.rb index f9d58f5d5b2..2cd8c60794a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -273,6 +273,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: post :merge post :cancel_merge_when_build_succeeds get :ci_status + get :ci_environments_status post :toggle_subscription post :remove_wip get :diff_for_path @@ -407,7 +408,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end - resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } + resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ } resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do member do diff --git a/config/routes/user.rb b/config/routes/user.rb index 54bbcb18f6a..dfb5d2a2ba4 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -33,5 +33,6 @@ scope(path: 'u/:username', get :projects get :contributed, as: :contributed_projects get :snippets + get :exists get '/', to: redirect('/%{username}') end diff --git a/doc/README.md b/doc/README.md index 7e3d9b00900..c30bf328003 100644 --- a/doc/README.md +++ b/doc/README.md @@ -49,6 +49,7 @@ - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. +- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests. - [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint. - [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs. diff --git a/doc/administration/monitoring/performance/img/request_profile_result.png b/doc/administration/monitoring/performance/img/request_profile_result.png Binary files differnew file mode 100644 index 00000000000..73e2fdcab67 --- /dev/null +++ b/doc/administration/monitoring/performance/img/request_profile_result.png diff --git a/doc/administration/monitoring/performance/img/request_profiling_token.png b/doc/administration/monitoring/performance/img/request_profiling_token.png Binary files differnew file mode 100644 index 00000000000..04d87567816 --- /dev/null +++ b/doc/administration/monitoring/performance/img/request_profiling_token.png diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md new file mode 100644 index 00000000000..c358dfbead2 --- /dev/null +++ b/doc/administration/monitoring/performance/request_profiling.md @@ -0,0 +1,16 @@ +# Request Profiling + +## Procedure +1. Grab the profiling token from `Monitoring > Requests Profiles` admin page +(highlighted in a blue in the image below). +![Profile token](img/request_profiling_token.png) +1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools + * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension + * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension + * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project` +1. Once request is finished (which will take a little longer than usual), you can +view the profiling output from `Monitoring > Requests Profiles` admin page. +![Profiling output](img/request_profile_result.png) + +## Cleaning up +Profiling output will be cleared out every day via a Sidekiq worker. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 59399861a97..84ea59ab687 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -159,7 +159,8 @@ Variables can be also defined on [job level](#job-variables). > Introduced in GitLab Runner v0.7.0. `cache` is used to specify a list of files and directories which should be -cached between builds. +cached between builds. You can only use paths that are within the project +workspace. **By default the caching is enabled per-job and per-branch.** @@ -606,8 +607,8 @@ You can see a simple example at https://gitlab.com/gitlab-examples/review-apps-n > - Build artifacts are only collected for successful builds by default. `artifacts` is used to specify a list of files and directories which should be -attached to the build after success. To pass artifacts between different builds, -see [dependencies](#dependencies). +attached to the build after success. You can only use paths that are within the +project workspace. To pass artifacts between different builds, see [dependencies](#dependencies). Below are some examples. diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 47ebcebc770..f07d2c9af2d 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -472,4 +472,4 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" [ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure" [graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle -[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png +[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
\ No newline at end of file diff --git a/doc/development/performance.md b/doc/development/performance.md index 7ff603e2c4a..65d34829025 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -34,10 +34,11 @@ graphs/dashboards. ## Tooling -GitLab provides two built-in tools to aid the process of improving performance: +GitLab provides built-in tools to aid the process of improving performance: * [Sherlock](profiling.md#sherlock) * [GitLab Performance Monitoring](../monitoring/performance/monitoring.md) +* [Request Profiling](../administration/monitoring/performance/request_profiling.md) GitLab employees can use GitLab.com's performance monitoring systems located at <http://performance.gitlab.net>, this requires you to log in using your diff --git a/doc/install/installation.md b/doc/install/installation.md index 1fa8678223a..c9acc9cdfb0 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -400,7 +400,7 @@ If you are not using Linux you may have to run `gmake` instead of cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.8.4 + sudo -u git -H git checkout v0.8.5 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md index 93320b40174..0d4be02ff5f 100644 --- a/doc/monitoring/performance/grafana_configuration.md +++ b/doc/monitoring/performance/grafana_configuration.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/grafana_configuration](../administration/monitoring/performance/grafana_configuration.md). +This document was moved to [administration/monitoring/performance/grafana_configuration](../../administration/monitoring/performance/grafana_configuration.md). diff --git a/doc/university/README.md b/doc/university/README.md index e71e49c33c8..f5a0dab39fe 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -64,6 +64,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter 1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/) 1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit) +1. [The GitLab Book Club](bookclub/index.md) #### 1.7 Community and Support diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md new file mode 100644 index 00000000000..c4229832e9f --- /dev/null +++ b/doc/university/bookclub/booklist.md @@ -0,0 +1,113 @@ +# Books + +List of books and resources, that may be worth reading. + +## Papers + +1. **The Humble Programmer** + + Edsger W. Dijkstra, 1972 ([paper](http://dl.acm.org/citation.cfm?id=361591)) + +## Programming + +1. **Design Patterns: Elements of Reusable Object-Oriented Software** + + Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994 ([amazon](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)) + +1. **Clean Code: A Handbook of Agile Software Craftsmanship** + + Robert C. "Uncle Bob" Martin, 2008 ([amazon](http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)) + +1. **Code Complete: A Practical Handbook of Software Construction**, 2nd Edition + + Steve McConnell, 2004 ([amazon](http://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670)) + +1. **The Pragmatic Programmer: From Journeyman to Master** + + Andrew Hunt, David Thomas, 1999 ([amazon](http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X)) + +1. **Working Effectively with Legacy Code** + + Michael Feathers, 2004 ([amazon](http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052)) + +1. **Eloquent Ruby** + + Russ Olsen, 2011 ([amazon](http://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional/dp/0321584104)) + +1. **Domain-Driven Design: Tackling Complexity in the Heart of Software** + + Eric Evans, 2003 ([amazon](http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215)) + +1. **How to Solve It: A New Aspect of Mathematical Method** + + Polya G. 1957 ([amazon](http://www.amazon.com/How-Solve-Mathematical-Princeton-Science/dp/069116407X)) + +1. **Software Creativity 2.0** + + Robert L. Glass, 2006 ([amazon](http://www.amazon.com/Software-Creativity-2-0-Robert-Glass/dp/0977213315)) + +1. **Object-Oriented Software Construction** + + Bertrand Meyer, 1997 ([amazon](http://www.amazon.com/Object-Oriented-Software-Construction-Book-CD-ROM/dp/0136291554)) + +1. **Refactoring: Improving the Design of Existing Code** + + Martin Fowler, Kent Beck, 1999 ([amazon](http://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672)) + +1. **Test Driven Development: By Example** + + Kent Beck, 2002 ([amazon](http://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530)) + +1. **Algorithms in C++: Fundamentals, Data Structure, Sorting, Searching** + + Robert Sedgewick, 1990 ([amazon](http://www.amazon.com/Algorithms-Parts-1-4-Fundamentals-Structure/dp/0201350882)) + +1. **Effective C++** + + Scott Mayers, 1996 ([amazon](http://www.amazon.com/Effective-Specific-Improve-Programs-Designs/dp/0321334876)) + +1. **Extreme Programming Explained: Embrace Change** + + Kent Beck, 1999 ([amazon](http://www.amazon.com/Extreme-Programming-Explained-Embrace-Change/dp/0321278658)) + +1. **The Art of Computer Programming** + + Donald E. Knuth, 1997 ([amazon](http://www.amazon.com/Computer-Programming-Volumes-1-4A-Boxed/dp/0321751043)) + +1. **Writing Efficient Programs** + + Jon Louis Bentley, 1982 ([amazon](http://www.amazon.com/Writing-Efficient-Programs-Prentice-Hall-Software/dp/013970244X)) + +1. **The Mythical Man-Month: Essays on Software Engineering** + + Frederick Phillips Brooks, 1975 ([amazon](http://www.amazon.com/Mythical-Man-Month-Essays-Software-Engineering/dp/0201006502)) + +1. **Peopleware: Productive Projects and Teams** 3rd Edition + + Tom DeMarco, Tim Lister, 2013 ([amazon](http://www.amazon.com/Peopleware-Productive-Projects-Teams-3rd/dp/0321934113)) + +1. **Principles Of Software Engineering Management** + + Tom Gilb, 1988 ([amazon](http://www.amazon.com/Principles-Software-Engineering-Management-Gilb/dp/0201192462)) + +## Other + +1. **Thinking, Fast and Slow** + + Daniel Kahneman, 2013 ([amazon](http://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555)) + +1. **The Social Animal** 11th Edition + + Elliot Aronson, 2011 ([amazon](http://www.amazon.com/Social-Animal-Elliot-Aronson/dp/1429233419)) + +1. **Influence: Science and Practice** 5th Edition + + Robert B. Cialdini, 2008 ([amazon](http://www.amazon.com/Influence-Practice-Robert-B-Cialdini/dp/0205609996)) + +1. **Getting to Yes: Negotiating Agreement Without Giving In** + + Roger Fisher, William L. Ury, Bruce Patton, 2011 ([amazon](http://www.amazon.com/Getting-Yes-Negotiating-Agreement-Without/dp/0143118757)) + +1. **How to Win Friends & Influence People** + + Dale Carnegie, 1981 ([amazon](http://www.amazon.com/How-Win-Friends-Influence-People/dp/0671027034)) diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md new file mode 100644 index 00000000000..022a61f4429 --- /dev/null +++ b/doc/university/bookclub/index.md @@ -0,0 +1,19 @@ +# The GitLab Book Club + +The Book Club is a casual meet-up to read and discuss books we like. +We'll find a time that suits most, if not all. + +See the [book list](booklist.md) for additional recommendations. + +## Currently reading : Books about remote work + +1. **Remote: Office not required** + + David Heinemeier Hansson and Jason Fried, 2013 + ([amazon](http://www.amazon.co.uk/Remote-Required-David-Heinemeier-Hansson/dp/0091954673)) + +1. **The Year Without Pants** + + Scott Berkun, 2013 ([ScottBerkun.com](http://scottberkun.com/yearwithoutpants/)) + +Any other books you'd like to suggest? Edit this page and add them to the queue. diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index 00d63c1b3c6..8940d14559b 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -84,7 +84,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.8.4 +sudo -u git -H git checkout v0.8.5 sudo -u git -H make ``` diff --git a/docker/README.md b/docker/README.md index ee1f32adc26..f9e12c5733b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,6 +2,6 @@ * The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/). * The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/). -* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/) +* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/) * The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker) -* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#build-docker-image) +* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image) diff --git a/features/groups.feature b/features/groups.feature index 49e939807b5..4044bd9be79 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -39,11 +39,6 @@ Feature: Groups When I visit group "Owned" merge requests page Then I should not see merge requests from the archived project - Scenario: I should see edit group "Owned" page - When I visit group "Owned" settings page - And I change group "Owned" name to "new-name" - Then I should see new group "Owned" name - Scenario: I edit group "Owned" avatar When I visit group "Owned" settings page And I change group "Owned" avatar diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 0c89a3db9ad..9396a76f0a2 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -105,7 +105,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps select "Developer", from: "access_level" end - click_button "Add users to group" + click_button "Add to group" end step 'I should see current user as "Developer"' do diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index d77945a6b9c..2b8cd030ace 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -70,7 +70,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps select "Developer", from: "access_level" end - click_button "Add users to project" + click_button "Add to project" end step 'I should see current user as "Developer"' do diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index e9b45823c67..cefc55d07ab 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -1,4 +1,5 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps + include WaitForAjax include SharedAuthentication include SharedPaths include SharedGroup @@ -13,7 +14,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps select "Reporter", from: "access_level" end - click_button "Add users to group" + click_button "Add to group" end step 'I select "Mike" as "Master"' do @@ -24,7 +25,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps select "Master", from: "access_level" end - click_button "Add users to group" + click_button "Add to group" end step 'I should see "Mike" in team list as "Reporter"' do @@ -47,7 +48,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps select "Reporter", from: "access_level" end - click_button "Add users to group" + click_button "Add to group" end step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do @@ -66,7 +67,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps select "Reporter", from: "access_level" end - click_button "Add users to group" + click_button "Add to group" end step 'I should see user "John Doe" in team list' do @@ -108,7 +109,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps step 'I search for \'Mary\' member' do page.within '.member-search-form' do fill_in 'search', with: 'Mary' - click_button 'Search' + find('.member-search-btn').click end end @@ -116,9 +117,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps member = mary_jane_member page.within "#group_member_#{member.id}" do - click_button 'Edit' select 'Developer', from: "member_access_level_#{member.id}" - click_on 'Save' + wait_for_ajax end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 4fa7d7c6567..0e81e99120b 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -73,18 +73,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps author: current_user end - step 'I change group "Owned" name to "new-name"' do - fill_in 'group_name', with: 'new-name' - fill_in 'group_path', with: 'new-name' - click_button "Save group" - end - - step 'I should see new group "Owned" name' do - page.within ".navbar-gitlab" do - expect(page).to have_content "new-name" - end - end - step 'I change group "Owned" avatar' do attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')) click_button "Save group" diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index b09ec86e5df..7490d2bc6e7 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -19,8 +19,8 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps end step 'page should have languages graphs' do - expect(page).to have_content "Ruby 66.63 %" - expect(page).to have_content "JavaScript 22.96 %" + expect(page).to have_content /Ruby 66.* %/ + expect(page).to have_content /JavaScript 22.* %/ end step 'page should have commits graphs' do diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index e920f5a706b..b21d0849ad1 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -22,7 +22,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end - click_button "Add users to project" + click_button "Add to project" end step 'I should see "Mike" in team list as "Reporter"' do @@ -36,10 +36,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I select "sjobs@apple.com" as "Reporter"' do page.within ".users-project-form" do - select2("sjobs@apple.com", from: "#user_ids", multiple: true) + find('#user_ids', visible: false).set('sjobs@apple.com') select "Reporter", from: "access_level" end - click_button "Add users to project" + click_button "Add to project" end step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do @@ -65,9 +65,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Dmitriy') project_member = project.project_members.find_by(user_id: user.id) page.within "#project_member_#{project_member.id}" do - click_button 'Edit' select "Reporter", from: "member_access_level_#{project_member.id}" - click_button "Save" end end @@ -112,7 +110,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I click link "Import team from another project"' do - click_link "Import members from another project" + click_link "Import" end When 'I submit "Website" project for import team' do @@ -144,8 +142,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I should see "Opensource" group user listing' do - expect(page).to have_content("Shared with OpenSource group, members with Master role (2)") - expect(page).to have_content(@os_user1.name) - expect(page).to have_content(@os_user2.name) + page.within '.project-members-groups' do + expect(page).to have_content('OpenSource') + expect(find('select').value).to eq('40') + end end end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb index 1ab308cfa55..8294bb1445f 100644 --- a/features/support/db_cleaner.rb +++ b/features/support/db_cleaner.rb @@ -1,6 +1,6 @@ require 'database_cleaner' -DatabaseCleaner.strategy = :truncation +DatabaseCleaner[:active_record].strategy = :truncation Spinach.hooks.before_scenario do DatabaseCleaner.start diff --git a/features/support/rerun.rb b/features/support/rerun.rb index 8b176c5be89..60b78f9d050 100644 --- a/features/support/rerun.rb +++ b/features/support/rerun.rb @@ -1,5 +1,7 @@ # The spinach-rerun-reporter doesn't define the on_undefined_step # See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb +require 'spinach-rerun-reporter' + module Spinach class Reporter class Rerun diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 9b71d335128..b14dd4f6e83 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -3,19 +3,28 @@ module API class Boards < Grape::API before { authenticate! } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get the project board + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success Entities::Board + end get ':id/boards' do authorize!(:read_board, user_project) present user_project.boards, with: Entities::Board end + params do + requires :board_id, type: Integer, desc: 'The ID of a board' + end segment ':id/boards/:board_id' do helpers do def project_board board = user_project.boards.first - if params[:board_id].to_i == board.id + if params[:board_id] == board.id board else not_found!('Board') @@ -27,29 +36,35 @@ module API end end - # Get the lists of a project board - # Does not include `backlog` and `done` lists + desc 'Get the lists of a project board' do + detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13' + success Entities::List + end get '/lists' do authorize!(:read_board, user_project) present board_lists, with: Entities::List end - # Get a list of a project board + desc 'Get a list of a project board' do + detail 'This feature was introduced in 8.13' + success Entities::List + end + params do + requires :list_id, type: Integer, desc: 'The ID of a list' + end get '/lists/:list_id' do authorize!(:read_board, user_project) present board_lists.find(params[:list_id]), with: Entities::List end - # Create a new board list - # - # Parameters: - # id (required) - The ID of a project - # label_id (required) - The ID of an existing label - # Example Request: - # POST /projects/:id/boards/:board_id/lists + desc 'Create a new board list' do + detail 'This feature was introduced in 8.13' + success Entities::List + end + params do + requires :label_id, type: Integer, desc: 'The ID of an existing label' + end post '/lists' do - required_attributes! [:label_id] - unless user_project.labels.exists?(params[:label_id]) render_api_error!({ error: "Label not found!" }, 400) end @@ -68,21 +83,21 @@ module API end end - # Moves a board list to a new position - # - # Parameters: - # id (required) - The ID of a project - # board_id (required) - The ID of a board - # position (required) - The position of the list - # Example Request: - # PUT /projects/:id/boards/:board_id/lists/:list_id + desc 'Moves a board list to a new position' do + detail 'This feature was introduced in 8.13' + success Entities::List + end + params do + requires :list_id, type: Integer, desc: 'The ID of a list' + requires :position, type: Integer, desc: 'The position of the list' + end put '/lists/:list_id' do list = project_board.lists.movable.find(params[:list_id]) authorize!(:admin_list, user_project) service = ::Boards::Lists::MoveService.new(user_project, current_user, - { position: params[:position].to_i }) + { position: params[:position] }) if service.execute(list) present list, with: Entities::List @@ -91,14 +106,13 @@ module API end end - # Delete a board list - # - # Parameters: - # id (required) - The ID of a project - # board_id (required) - The ID of a board - # list_id (required) - The ID of a board list - # Example Request: - # DELETE /projects/:id/boards/:board_id/lists/:list_id + desc 'Delete a board list' do + detail 'This feature was introduced in 8.13' + success Entities::List + end + params do + requires :list_id, type: Integer, desc: 'The ID of a board list' + end delete "/lists/:list_id" do authorize!(:admin_list, user_project) diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 19df13d8aac..832b04a3bb1 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -8,18 +8,19 @@ module API 'issues' => ->(id) { find_project_issue(id) } } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do ISSUABLE_TYPES.each do |type, finder| type_id_str = "#{type.singularize}_id".to_sym - # Create a todo on an issuable - # - # Parameters: - # id (required) - The ID of a project - # issuable_id (required) - The ID of an issuable - # Example Request: - # POST /projects/:id/issues/:issuable_id/todo - # POST /projects/:id/merge_requests/:issuable_id/todo + desc 'Create a todo on an issuable' do + success Entities::Todo + end + params do + requires type_id_str, type: Integer, desc: 'The ID of an issuable' + end post ":id/#{type}/:#{type_id_str}/todo" do issuable = instance_exec(params[type_id_str], &finder) todo = TodoService.new.mark_todo(issuable, current_user).first @@ -40,25 +41,21 @@ module API end end - # Get a todo list - # - # Example Request: - # GET /todos - # + desc 'Get a todo list' do + success Entities::Todo + end get do todos = find_todos present paginate(todos), with: Entities::Todo, current_user: current_user end - # Mark a todo as done - # - # Parameters: - # id: (required) - The ID of the todo being marked as done - # - # Example Request: - # DELETE /todos/:id - # + desc 'Mark a todo as done' do + success Entities::Todo + end + params do + requires :id, type: Integer, desc: 'The ID of the todo being marked as done' + end delete ':id' do todo = current_user.todos.find(params[:id]) TodoService.new.mark_todos_as_done([todo], current_user) @@ -66,11 +63,7 @@ module API present todo.reload, with: Entities::Todo, current_user: current_user end - # Mark all todos as done - # - # Example Request: - # DELETE /todos - # + desc 'Mark all todos as done' delete do todos = find_todos TodoService.new.mark_todos_as_done(todos, current_user) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 6924a293da8..ce048a36fa0 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,6 +1,6 @@ module Banzai module Renderer - extend self + module_function # Convert a Markdown String into an HTML-safe String of HTML # @@ -141,8 +141,6 @@ module Banzai end.html_safe end - private - def cacheless_render(text, context = {}) Gitlab::Metrics.measure(:banzai_cacheless_render) do result = render_result(text, context) diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index e4d996a3fb6..9b74364849e 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -113,17 +113,18 @@ module ExtractsPath @id = get_id @ref, @path = extract_ref(@id) @repo = @project.repository - if @options[:extended_sha1].blank? - @commit = @repo.commit(@ref) - else - @commit = @repo.commit(@options[:extended_sha1]) - end - if @path.empty? && !@commit - @id = @ref = extract_ref_without_atom(@id) + if @options[:extended_sha1].present? + @commit = @repo.commit(@options[:extended_sha1]) + else @commit = @repo.commit(@ref) - request.format = :atom if @commit + if @path.empty? && !@commit && @id.ends_with?('.atom') + @id = @ref = extract_ref_without_atom(@id) + @commit = @repo.commit(@ref) + + request.format = :atom if @commit + end end raise InvalidPathError unless @commit diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index d0060fbaca1..9cec71a3222 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -47,8 +47,8 @@ module Gitlab unless File.size?(secret_file) # Generate a new token of 16 random hexadecimal characters and store it in secret_file. - token = SecureRandom.hex(16) - File.write(secret_file, token) + @secret_token = SecureRandom.hex(16) + File.write(secret_file, @secret_token) end link_path = File.join(shell_path, '.gitlab_shell_secret') diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 84298f8bef4..d509f0f2b96 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -756,4 +756,34 @@ describe Projects::MergeRequestsController do post_assign_issues end end + + describe 'GET ci_environments_status' do + context 'when the environment is from a forked project' do + let!(:forked) { create(:project) } + let!(:environment) { create(:environment, project: forked) } + let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } + let(:json_response) { JSON.parse(response.body) } + let(:admin) { create(:admin) } + + let(:merge_request) do + create(:forked_project_link, forked_to_project: forked, + forked_from_project: project) + + create(:merge_request, source_project: forked, target_project: project) + end + + before do + forked.team << [user, :master] + + get :ci_environments_status, + namespace_id: merge_request.project.namespace.to_param, + project_id: merge_request.project.to_param, + id: merge_request.iid, format: 'json' + end + + it 'links to the environment on that project' do + expect(json_response.first['url']).to match /#{forked.path_with_namespace}/ + end + end + end end diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb index 10d3713f19f..d811b05b0c3 100644 --- a/spec/features/groups/members/owner_manages_access_requests_spec.rb +++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb @@ -41,7 +41,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do def expect_visible_access_request(group, user) expect(group.requesters.exists?(user_id: user)).to be_truthy - expect(page).to have_content "#{group.name} access requests 1" + expect(page).to have_content "Users requesting access to #{group.name} 1" expect(page).to have_content user.name end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index c54ec2563ad..13bfe90302c 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -11,67 +11,99 @@ feature 'Group', feature: true do end end - describe 'creating a group with space in group path' do - it 'renders new group form with validation errors' do - visit new_group_path - fill_in 'Group path', with: 'space group' + describe 'create a group' do + before { visit new_group_path } - click_button 'Create group' + describe 'with space in group path' do + it 'renders new group form with validation errors' do + fill_in 'Group path', with: 'space group' + click_button 'Create group' - expect(current_path).to eq(groups_path) - expect(page).to have_namespace_error_message + expect(current_path).to eq(groups_path) + expect(page).to have_namespace_error_message + end end - end - - describe 'creating a group with .atom at end of group path' do - it 'renders new group form with validation errors' do - visit new_group_path - fill_in 'Group path', with: 'atom_group.atom' - click_button 'Create group' + describe 'with .atom at end of group path' do + it 'renders new group form with validation errors' do + fill_in 'Group path', with: 'atom_group.atom' + click_button 'Create group' - expect(current_path).to eq(groups_path) - expect(page).to have_namespace_error_message + expect(current_path).to eq(groups_path) + expect(page).to have_namespace_error_message + end + end + + describe 'with .git at end of group path' do + it 'renders new group form with validation errors' do + fill_in 'Group path', with: 'git_group.git' + click_button 'Create group' + + expect(current_path).to eq(groups_path) + expect(page).to have_namespace_error_message + end end end - - describe 'creating a group with .git at end of group path' do - it 'renders new group form with validation errors' do - visit new_group_path - fill_in 'Group path', with: 'git_group.git' - click_button 'Create group' + describe 'group edit' do + let(:group) { create(:group) } + let(:path) { edit_group_path(group) } + let(:new_name) { 'new-name' } + + before { visit path } + + it 'saves new settings' do + fill_in 'group_name', with: new_name + click_button 'Save group' + + expect(page).to have_content 'successfully updated' + expect(find('#group_name').value).to eq(new_name) - expect(current_path).to eq(groups_path) - expect(page).to have_namespace_error_message + page.within ".navbar-gitlab" do + expect(page).to have_content new_name + end + end + + it 'removes group' do + click_link 'Remove Group' + + expect(page).to have_content "scheduled for deletion" end end - describe 'description' do + describe 'group page with markdown description' do let(:group) { create(:group) } let(:path) { group_path(group) } it 'parses Markdown' do group.update_attribute(:description, 'This is **my** group') + visit path + expect(page).to have_css('.description > p > strong') end it 'passes through html-pipeline' do group.update_attribute(:description, 'This group is the :poop:') + visit path + expect(page).to have_css('.description > p > img') end it 'sanitizes unwanted tags' do group.update_attribute(:description, '# Group Description') + visit path + expect(page).not_to have_css('.description h1') end it 'permits `rel` attribute on links' do group.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.description a[rel]') end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 2523b4b7898..996f39ea06d 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -29,7 +29,7 @@ feature 'Login', feature: true do describe 'with two-factor authentication' do def enter_code(code) - fill_in 'Two-Factor Authentication code', with: code + fill_in 'user_otp_attempt', with: code click_button 'Verify code' end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 4d5d4aa121a..a506624b30d 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -45,7 +45,7 @@ feature 'Merge request created from fork' do page.within('.merge-request-tabs') { click_link 'Builds' } wait_for_ajax - page.within('table.builds') do + page.within('table.ci-table') do expect(page).to have_content 'rspec' expect(page).to have_content 'spinach' end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index cb3cea3fd51..7b8af555f0e 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -20,7 +20,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do login_with(user) visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - + after do wait_for_ajax end @@ -34,7 +34,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do expect(page).to have_content 'Your commands have been executed!' expect(merge_request.reload.work_in_progress?).to eq true - end + end it 'removes the WIP: prefix from the title' do merge_request.title = merge_request.wip_title @@ -45,7 +45,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do expect(page).to have_content 'Your commands have been executed!' expect(merge_request.reload.work_in_progress?).to eq false - end + end end context 'when the current user cannot toggle the WIP prefix' do diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb new file mode 100644 index 00000000000..8e23ec50d4a --- /dev/null +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +feature 'Widget Deployments Header', feature: true, js: true do + include WaitForAjax + + describe 'when deployed to an environment' do + let(:project) { merge_request.target_project } + let(:merge_request) { create(:merge_request, :merged) } + let(:environment) { create(:environment, project: project) } + let!(:deployment) do + create(:deployment, environment: environment, sha: project.commit('master').id) + end + + before do + login_as :admin + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'displays that the environment is deployed' do + wait_for_ajax + + expect(page).to have_content("Deployed to #{environment.name}") + expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) + end + end +end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index cd79c4f512d..d886909ce85 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -15,6 +15,7 @@ feature 'issuable templates', feature: true, js: true do let(:template_content) { 'this is a test "bug" template' } let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) } let(:issue) { create(:issue, author: user, assignee: user, project: project) } + let(:description_addition) { ' appending to description' } background do project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) @@ -26,7 +27,26 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template(template_content) + preview_template + save_changes + end + + scenario 'user selects "bug" template and then "no template"' do + select_template 'bug' + wait_for_ajax + select_option 'No template' + wait_for_ajax + preview_template('') + save_changes('') + end + + scenario 'user selects "bug" template, edits description and then selects "reset template"' do + select_template 'bug' + wait_for_ajax + find_field('issue_description').send_keys(description_addition) + preview_template(template_content + description_addition) + select_option 'Reset template' + preview_template save_changes end @@ -37,7 +57,7 @@ feature 'issuable templates', feature: true, js: true do wait_for_ajax end_height = page.evaluate_script('$(".markdown-area").outerHeight()') - + expect(end_height).not_to eq(start_height) end end @@ -75,7 +95,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "feature-proposal" template' do select_template 'feature-proposal' wait_for_ajax - preview_template(template_content) + preview_template save_changes end end @@ -102,25 +122,31 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects template' do select_template 'feature-proposal' wait_for_ajax - preview_template(template_content) + preview_template save_changes end end end end - def preview_template(expected_content) + def preview_template(expected_content = template_content) click_link 'Preview' expect(page).to have_content expected_content + click_link 'Write' end - def save_changes + def save_changes(expected_content = template_content) click_button "Save changes" - expect(page).to have_content template_content + expect(page).to have_content expected_content end def select_template(name) first('.js-issuable-selector').click first('.js-issuable-selector-wrap .dropdown-content a', text: name).click end + + def select_option(name) + first('.js-issuable-selector').click + first('.js-issuable-selector-wrap .dropdown-footer-list a', text: name).click + end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb new file mode 100644 index 00000000000..cc2f695211c --- /dev/null +++ b/spec/features/projects/members/group_links_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +feature 'Projects > Members > Anonymous user sees members', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:group) { create(:group, :public) } + let(:project) { create(:empty_project, :public) } + + background do + project.team << [user, :master] + @group_link = create(:project_group_link, project: project, group: group) + + login_as(user) + visit namespace_project_project_members_path(project.namespace, project) + end + + it 'updates group access level' do + select 'Guest', from: "member_access_level_#{group.id}" + wait_for_ajax + + visit namespace_project_project_members_path(project.namespace, project) + + expect(page).to have_select("member_access_level_#{group.id}", selected: 'Guest') + end + + it 'updates expiry date' do + tomorrow = Date.today + 3 + + fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F") + wait_for_ajax + + page.within(find('li.group_member')) do + expect(page).to have_content('Expires in') + end + end + + it 'deletes group link' do + page.within(first('.group_member')) do + find('.btn-remove').click + end + wait_for_ajax + + expect(page).not_to have_selector('.group_member') + end + + context 'search' do + it 'finds no results' do + page.within '.member-search-form' do + fill_in 'search', with: 'testing 123' + find('.member-search-btn').click + end + + expect(page).not_to have_selector('.group_member') + end + + it 'finds results' do + page.within '.member-search-form' do + fill_in 'search', with: group.name + find('.member-search-btn').click + end + + expect(page).to have_selector('.group_member', count: 1) + end + end +end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 430c384ac2e..27a83fdcd1f 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do + include WaitForAjax include Select2Helper include ActiveSupport::Testing::TimeHelpers @@ -20,7 +21,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: page.within '.users-project-form' do select2(new_member.id, from: '#user_ids', multiple: true) fill_in 'expires_at', with: '2016-08-10' - click_on 'Add users to project' + click_on 'Add to project' end page.within '.project_member:first-child' do @@ -35,9 +36,8 @@ feature 'Projects > Members > Master adds member with expiration date', feature: visit namespace_project_project_members_path(project.namespace, project) page.within '.project_member:first-child' do - click_on 'Edit' - fill_in 'Access expiration date', with: '2016-08-09' - click_on 'Save' + find('.js-access-expiration-date').set '2016-08-09' + wait_for_ajax expect(page).to have_content('Expires in 3 days') end end diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index f7fcd9b6731..d15376931c3 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do def expect_visible_access_request(project, user) expect(project.requesters.exists?(user_id: user)).to be_truthy - expect(page).to have_content "#{project.name} access requests 1" + expect(page).to have_content "Users requesting access to #{project.name} 1" expect(page).to have_content user.name end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index a752c1d7235..65544f79eba 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -14,7 +14,7 @@ feature 'Signup', feature: true do fill_in 'new_user_username', with: user.username fill_in 'new_user_email', with: user.email fill_in 'new_user_password', with: user.password - click_button "Sign up" + click_button "Register" expect(current_path).to eq users_almost_there_path expect(page).to have_content("Please check your email to confirm your account") @@ -33,7 +33,7 @@ feature 'Signup', feature: true do fill_in 'new_user_username', with: user.username fill_in 'new_user_email', with: user.email fill_in 'new_user_password', with: user.password - click_button "Sign up" + click_button "Register" expect(current_path).to eq dashboard_projects_path expect(page).to have_content("Welcome! You have signed up successfully.") @@ -52,7 +52,7 @@ feature 'Signup', feature: true do fill_in 'new_user_username', with: user.username fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_password', with: user.password - click_button "Sign up" + click_button "Register" expect(current_path).to eq user_registration_path expect(page).to have_content("error prohibited this user from being saved") @@ -69,7 +69,7 @@ feature 'Signup', feature: true do fill_in 'new_user_username', with: user.username fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_password', with: user.password - click_button "Sign up" + click_button "Register" expect(current_path).to eq user_registration_path expect(page.body).not_to match(/#{user.password}/) diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index ff6933dc8d9..b750f27ea72 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -160,7 +160,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user) @u2f_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" @@ -174,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user) @u2f_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" @@ -186,7 +186,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: login_with(user, remember: true) @u2f_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') within 'div#js-authenticate-u2f' do @@ -209,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: # Try authenticating user with the old U2F device login_as(current_user) @u2f_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" @@ -230,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: # Try authenticating user with the same U2F device login_as(current_user) @u2f_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" @@ -244,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name) login_as(user) unregistered_device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" @@ -271,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: [first_device, second_device].each do |device| login_as(user) device.respond_to_u2f_authentication - click_on "Login Via U2F Device" + click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 6498b7317b4..ec4c4d62f53 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,15 +1,16 @@ require 'spec_helper' -feature 'Users', feature: true do +feature 'Users', feature: true, js: true do let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path + click_link 'Register' fill_in 'new_user_name', with: 'Name Surname' fill_in 'new_user_username', with: 'Great' fill_in 'new_user_email', with: 'name@mail.com' fill_in 'new_user_password', with: 'password1234' - expect { click_button 'Sign up' }.to change { User.count }.by(1) + expect { click_button 'Register' }.to change { User.count }.by(1) end scenario 'Successful user signin invalidates password reset token' do @@ -31,11 +32,12 @@ feature 'Users', feature: true do scenario 'Should show one error if email is already taken' do visit new_user_session_path + click_link 'Register' fill_in 'new_user_name', with: 'Another user name' fill_in 'new_user_username', with: 'anotheruser' fill_in 'new_user_email', with: user.email fill_in 'new_user_password', with: '12341234' - expect { click_button 'Sign up' }.to change { User.count }.by(0) + expect { click_button 'Register' }.to change { User.count }.by(0) expect(page).to have_text('Email has already been taken') expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' end @@ -51,6 +53,28 @@ feature 'Users', feature: true do end end + feature 'username validation' do + include WaitForAjax + let(:loading_icon) { '.fa.fa-spinner' } + let(:username_input) { 'new_user_username' } + + before(:each) do + visit new_user_session_path + click_link 'Register' + end + scenario 'shows an error border if the username already exists' do + fill_in username_input, with: user.username + wait_for_ajax + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + scenario 'doesn\'t show an error border if the username is available' do + fill_in username_input, with: 'new-user' + wait_for_ajax + expect(find('#new_user_username')).not_to have_css '.gl-field-error-outline' + end + end + def errors_on_page(page) page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n") end diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml new file mode 100644 index 00000000000..2526e5e33a5 --- /dev/null +++ b/spec/javascripts/fixtures/gl_field_errors.html.haml @@ -0,0 +1,15 @@ +%form.show-gl-field-errors{action: 'submit', method: 'post'} + .form-group + %input.required-text{required: true, type: 'text'} Text + .form-group + %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email + .form-group + %input.password{type: 'password', required: true} Password + .form-group + %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric + .form-group + %input.hidden{ type:'hidden' } + .form-group + %input.custom.no-gl-field-errors{ 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 new file mode 100644 index 00000000000..36feb2b2aa5 --- /dev/null +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -0,0 +1,111 @@ +//= require jquery +//= require gl_field_errors + +((global) => { + fixture.preload('gl_field_errors.html'); + + describe('GL Style Field Errors', function() { + beforeEach(function() { + fixture.load('gl_field_errors.html'); + const $form = this.$form = $('form.show-gl-field-errors'); + this.fieldErrors = new global.GlFieldErrors($form); + }); + + it('should properly initialize the form', function() { + expect(this.$form).toBeDefined(); + expect(this.$form.length).toBe(1); + expect(this.fieldErrors).toBeDefined(); + const inputs = this.fieldErrors.state.inputs; + expect(inputs.length).toBe(5); + }); + + it('should ignore elements with custom error handling', function() { + const customErrorFlag = 'no-gl-field-errors'; + const customErrorElem = $(`.${customErrorFlag}`); + + expect(customErrorElem.length).toBe(1); + + const customErrors = this.fieldErrors.state.inputs.filter((input) => { + return input.inputElement.hasClass(customErrorFlag); + }); + expect(customErrors.length).toBe(0); + }); + + it('should not show any errors before submit attempt', function() { + this.$form.find('.email').val('not-a-valid-email').keyup(); + this.$form.find('.text-required').val('').keyup(); + this.$form.find('.alphanumberic').val('?---*').keyup(); + + const errorsShown = this.$form.find('.gl-field-error-outline'); + expect(errorsShown.length).toBe(0); + }); + + it('should show errors when input valid is submitted', function() { + this.$form.find('.email').val('not-a-valid-email').keyup(); + this.$form.find('.text-required').val('').keyup(); + this.$form.find('.alphanumberic').val('?---*').keyup(); + + this.$form.submit(); + + const errorsShown = this.$form.find('.gl-field-error-outline'); + expect(errorsShown.length).toBe(4); + }); + + it('should properly track validity state on input after invalid submission attempt', function() { + this.$form.submit(); + + const emailInputModel = this.fieldErrors.state.inputs[1]; + const fieldState = emailInputModel.state; + const emailInputElement = emailInputModel.inputElement; + + // No input + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then empty input + emailInputElement.val('').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + }); + + it('should properly infer error messages', function() { + this.$form.submit(); + const trackedInputs = this.fieldErrors.state.inputs; + const inputHasTitle = trackedInputs[1]; + const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error'); + const inputNoTitle = trackedInputs[2]; + const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error'); + + expect(noTitleErrorElem.text()).toBe('This field is required.'); + expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); + }); + + }); + +})(window.gl || (window.gl = {})); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 395032a7416..96ee5235acf 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,5 +1,6 @@ /*= require merge_request_tabs */ +//= require breakpoints (function() { describe('MergeRequestTabs', function() { diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 17b32914ec3..c9175e2b704 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,5 +1,5 @@ - /*= require merge_request_widget */ +/*= require lib/utils/jquery.timeago.js */ (function() { describe('MergeRequestWidget', function() { @@ -8,6 +8,7 @@ window.notify = function() {}; this.opts = { ci_status_url: "http://sampledomain.local/ci/getstatus", + ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus", ci_status: "", ci_message: { normal: "Build {{status}} for \"{{title}}\"", @@ -20,17 +21,48 @@ gitlab_icon: "gitlab_logo.png", builds_path: "http://sampledomain.local/sampleBuildsPath" }; - this["class"] = new MergeRequestWidget(this.opts); - return this.ciStatusData = { - "title": "Sample MR title", - "sha": "12a34bc5", - "status": "success", - "coverage": 98 - }; + this["class"] = new window.gl.MergeRequestWidget(this.opts); }); + + describe('getCIEnvironmentsStatus', function() { + beforeEach(function() { + this.ciEnvironmentsStatusData = [{ + created_at: '2016-09-12T13:38:30.636Z', + environment_id: 1, + environment_name: 'env1', + external_url: 'https://test-url.com', + external_url_formatted: 'test-url.com' + }]; + + spyOn(jQuery, 'getJSON').and.callFake((req, cb) => { + cb(this.ciEnvironmentsStatusData); + }); + }); + + it('should call renderEnvironments when the environments property is set', function() { + const spy = spyOn(this.class, 'renderEnvironments').and.stub(); + this.class.getCIEnvironmentsStatus(); + expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData); + }); + + it('should not call renderEnvironments when the environments property is not set', function() { + this.ciEnvironmentsStatusData = null; + const spy = spyOn(this.class, 'renderEnvironments').and.stub(); + this.class.getCIEnvironmentsStatus(); + expect(spy).not.toHaveBeenCalled(); + }); + }); + return describe('getCIStatus', function() { beforeEach(function() { - return spyOn(jQuery, 'getJSON').and.callFake((function(_this) { + this.ciStatusData = { + "title": "Sample MR title", + "sha": "12a34bc5", + "status": "success", + "coverage": 98 + }; + + spyOn(jQuery, 'getJSON').and.callFake((function(_this) { return function(req, cb) { return cb(_this.ciStatusData); }; @@ -61,10 +93,10 @@ this["class"].getCIStatus(false); return expect(spy).not.toHaveBeenCalled(); }); - return it('should not display a notification on the first check after the widget has been created', function() { + it('should not display a notification on the first check after the widget has been created', function() { var spy; spy = spyOn(window, 'notify'); - this["class"] = new MergeRequestWidget(this.opts); + this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); }); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 7ce3884f844..784b43d4846 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -21,7 +21,7 @@ setupButton = this.container.find("#js-login-u2f-device"); setupMessage = this.container.find("p"); expect(setupMessage.text()).toContain('Insert your security key'); - expect(setupButton.text()).toBe('Login Via U2F Device'); + expect(setupButton.text()).toBe('Sign in via U2F device'); setupButton.trigger('click'); inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 6b1867a44e1..e172ee8e590 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -64,6 +64,23 @@ describe Environment, models: true do end end + describe '#first_deployment_for' do + let(:project) { create(:project) } + let!(:environment) { create(:environment, project: project) } + let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } + let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } + let(:head_commit) { project.commit } + let(:commit) { project.commit.parent } + + it 'returns deployment id for the environment' do + expect(environment.first_deployment_for(commit)).to eq deployment1 + end + + it 'return nil when no deployment is found' do + expect(environment.first_deployment_for(head_commit)).to eq nil + end + end + describe '#environment_type' do subject { environment.environment_type } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 308a00db9cd..67dbcc362f6 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -228,7 +228,6 @@ describe Project, models: true do describe 'Respond to' do it { is_expected.to respond_to(:url_to_repo) } it { is_expected.to respond_to(:repo_exists?) } - it { is_expected.to respond_to(:update_merge_requests) } it { is_expected.to respond_to(:execute_hooks) } it { is_expected.to respond_to(:owner) } it { is_expected.to respond_to(:path_with_namespace) } @@ -389,26 +388,6 @@ describe Project, models: true do end end - describe '#update_merge_requests' do - let(:project) { create(:project) } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:key) { create(:key, user_id: project.owner.id) } - let(:prev_commit_id) { merge_request.commits.last.id } - let(:commit_id) { merge_request.commits.first.id } - - it 'closes merge request if last commit from source branch was pushed to target branch' do - project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user) - merge_request.reload - expect(merge_request.merged?).to be_truthy - end - - it 'updates merge request commits with new one if pushed to source branch' do - project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user) - merge_request.reload - expect(merge_request.diff_head_sha).to eq(commit_id) - end - end - describe '.find_with_namespace' do context 'with namespace' do before do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4b80efbe12b..f977cf73673 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -7,15 +7,18 @@ describe Repository, models: true do let(:project) { create(:project) } let(:repository) { project.repository } let(:user) { create(:user) } + let(:commit_options) do author = repository.user_to_committer(user) { message: 'Test message', committer: author, author: author } end + let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) merge_commit_id = repository.merge(user, merge_request, commit_options) repository.commit(merge_commit_id) end + let(:author_email) { FFaker::Internet.email } # I have to remove periods from the end of the name @@ -90,6 +93,26 @@ describe Repository, models: true do end end + describe '#ref_name_for_sha' do + context 'ref found' do + it 'returns the ref' do + allow_any_instance_of(Gitlab::Popen).to receive(:popen). + and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]) + + expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77' + end + end + + context 'ref not found' do + it 'returns nil' do + allow_any_instance_of(Gitlab::Popen).to receive(:popen). + and_return(["", 0]) + + expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil + end + end + end + describe '#last_commit_for_path' do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5a1ed7d4a25..27f0fd22ae6 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -412,9 +412,10 @@ describe 'Git HTTP requests', lib: true do context "when the params are anything else" do let(:params) { { service: 'git-implode-pack' } } + before { get path, params } - it "fails to find a route" do - expect { get(path, params) }.to raise_error(ActionController::RoutingError) + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) end end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 0ee1c811dfb..488dc1a63b0 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -270,6 +270,12 @@ describe "Groups", "routing" do expect(get('/1')).to route_to('groups#show', id: '1') end + + it "also display group#show with dot in the path" do + allow(Group).to receive(:find_by_path).and_return(true) + + expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot') + end end describe HealthCheckController, 'routing' do diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 343b4385bf2..5fe56e7725f 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -84,11 +84,22 @@ describe CreateDeploymentService, services: true do expect(subject).to be_persisted end end + + context 'when project was removed' do + let(:project) { nil } + + it 'does not create deployment or environment' do + expect { subject }.not_to raise_error + + expect(Environment.count).to be_zero + expect(Deployment.count).to be_zero + end + end end describe 'processing of builds' do let(:environment) { nil } - + shared_examples 'does not create environment and deployment' do it 'does not create a new environment' do expect { subject }.not_to change { Environment.count } @@ -133,12 +144,12 @@ describe CreateDeploymentService, services: true do context 'without environment specified' do let(:build) { create(:ci_build, project: project) } - + it_behaves_like 'does not create environment and deployment' do subject { build.success } end end - + context 'when environment is specified' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 8e3e12114f2..dd2a9e9903a 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -184,8 +184,8 @@ describe GitPushService, services: true do context "Updates merge requests" do it "when pushing a new branch for the first time" do - expect(project).to receive(:update_merge_requests). - with(@blankrev, 'newrev', 'refs/heads/master', user) + expect(UpdateMergeRequestsWorker).to receive(:perform_async). + with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master') execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 5b4e4908add..e515bc9f89c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -62,7 +62,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.notes).not_to be_empty } it { expect(@merge_request).to be_open } - it { expect(@merge_request.merge_when_build_succeeds).to be_falsey} + it { expect(@merge_request.merge_when_build_succeeds).to be_falsey } + it { expect(@merge_request.diff_head_sha).to eq(@newrev) } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } it { expect(@build_failed_todo).to be_done } diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb deleted file mode 100644 index 86980f59cd8..00000000000 --- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe 'projects/merge_requests/widget/_heading' do - include Devise::Test::ControllerHelpers - - context 'when released to an environment' do - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do - create(:deployment, environment: environment, sha: project.commit('master').id) - end - - before do - assign(:merge_request, merge_request) - assign(:project, project) - - allow(view).to receive(:can?).and_return(true) - - render - end - - it 'displays that the environment is deployed' do - expect(rendered).to match("Deployed to") - expect(rendered).to match("#{environment.name}") - end - end -end diff --git a/spec/workers/build_coverage_worker_spec.rb b/spec/workers/build_coverage_worker_spec.rb new file mode 100644 index 00000000000..ba20488f663 --- /dev/null +++ b/spec/workers/build_coverage_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildCoverageWorker do + describe '#perform' do + context 'when build exists' do + let!(:build) { create(:ci_build) } + + it 'updates code coverage' do + expect_any_instance_of(Ci::Build) + .to receive(:update_coverage) + + described_class.new.perform(build.id) + end + end + + context 'when build does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb new file mode 100644 index 00000000000..2868167c7d4 --- /dev/null +++ b/spec/workers/build_finished_worker_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe BuildFinishedWorker do + describe '#perform' do + context 'when build exists' do + let(:build) { create(:ci_build) } + + it 'calculates coverage and calls hooks' do + expect(BuildCoverageWorker) + .to receive(:new).ordered.and_call_original + expect(BuildHooksWorker) + .to receive(:new).ordered.and_call_original + + expect_any_instance_of(BuildCoverageWorker) + .to receive(:perform) + expect_any_instance_of(BuildHooksWorker) + .to receive(:perform) + + described_class.new.perform(build.id) + end + end + + context 'when build does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb new file mode 100644 index 00000000000..97654a93f5c --- /dev/null +++ b/spec/workers/build_hooks_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildHooksWorker do + describe '#perform' do + context 'when build exists' do + let!(:build) { create(:ci_build) } + + it 'calls build hooks' do + expect_any_instance_of(Ci::Build) + .to receive(:execute_hooks) + + described_class.new.perform(build.id) + end + end + + context 'when build does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb new file mode 100644 index 00000000000..dba70883130 --- /dev/null +++ b/spec/workers/build_success_worker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe BuildSuccessWorker do + describe '#perform' do + context 'when build exists' do + context 'when build belogs to the environment' do + let!(:build) { create(:ci_build, environment: 'production') } + + it 'executes deployment service' do + expect_any_instance_of(CreateDeploymentService) + .to receive(:execute) + + described_class.new.perform(build.id) + end + end + + context 'when build is not associated with project' do + let!(:build) { create(:ci_build, project: nil) } + + it 'does not create deployment' do + expect_any_instance_of(CreateDeploymentService) + .not_to receive(:execute) + + described_class.new.perform(build.id) + end + end + end + + context 'when build does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb new file mode 100644 index 00000000000..035e329839f --- /dev/null +++ b/spec/workers/pipeline_hooks_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe PipelineHooksWorker do + describe '#perform' do + context 'when pipeline exists' do + let(:pipeline) { create(:ci_pipeline) } + + it 'executes hooks for the pipeline' do + expect_any_instance_of(Ci::Pipeline) + .to receive(:execute_hooks) + + described_class.new.perform(pipeline.id) + end + end + + context 'when pipeline does not exist' do + it 'does not raise exception' do + expect { described_class.new.perform(123) } + .not_to raise_error + end + end + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index ffeaafe654a..984acdade36 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -92,7 +92,13 @@ describe PostReceive do allow(Project).to receive(:find_with_namespace).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - expect(project).to receive(:update_merge_requests) + + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + + it "enqueues a UpdateMergeRequestsWorker job" do + allow(Project).to receive(:find_with_namespace).and_return(project) + expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) PostReceive.new.perform(pwd(project), key_id, base64_changes) end diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb new file mode 100644 index 00000000000..c78a69eda67 --- /dev/null +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe UpdateMergeRequestsWorker do + include RepoHelpers + + let(:project) { create(:project) } + let(:user) { create(:user) } + + subject { described_class.new } + + describe '#perform' do + let(:oldrev) { "123456" } + let(:newrev) { "789012" } + let(:ref) { "refs/heads/test" } + + def perform + subject.perform(project.id, user.id, oldrev, newrev, ref) + end + + it 'executes MergeRequests::RefreshService with expected values' do + expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original + expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref) + + perform + end + + it 'executes SystemHooksService with expected values' do + push_data = double('push_data') + expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data) + + system_hook_service = double('system_hook_service') + expect(SystemHooksService).to receive(:new).and_return(system_hook_service) + expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks) + + perform + end + end +end |